├── .eslintignore
├── .eslintrc.cjs
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── .vscode
├── launch.json
└── settings.json
├── LICENSE
├── README.md
├── bun.lockb
├── jsconfig.json
├── package.json
├── postcss.config.js
├── src
├── app.d.ts
├── app.html
├── lib
│ ├── components
│ │ ├── .gitkeep
│ │ ├── Animated.svelte
│ │ └── PixelMap.svelte
│ ├── constants.js
│ ├── createAnimation.js
│ ├── createInvalidHeightError.js
│ ├── createInvalidWidthError.js
│ ├── index.js
│ ├── types.js
│ └── uuid.js
└── routes
│ └── +page.svelte
├── static
└── favicon.png
├── svelte.config.js
├── tailwind.config.js
└── vite.config.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 | /src/app.html
10 |
11 | # Ignore files for PNPM, NPM and YARN
12 | pnpm-lock.yaml
13 | package-lock.json
14 | yarn.lock
15 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /**
2 | * @type {import("eslint").Linter.Config}
3 | */
4 | module.exports = {
5 | root: true,
6 | extends: [
7 | 'eslint:recommended',
8 | 'plugin:@typescript-eslint/recommended',
9 | 'plugin:svelte/recommended',
10 | 'prettier',
11 | ],
12 | parser: '@typescript-eslint/parser',
13 | plugins: ['@typescript-eslint'],
14 | parserOptions: {
15 | sourceType: 'module',
16 | ecmaVersion: 2024,
17 | extraFileExtensions: ['.svelte'],
18 | },
19 | env: {
20 | browser: true,
21 | es2017: true,
22 | node: true,
23 | },
24 | globals: {
25 | Bun: 'readonly',
26 | },
27 | rules: {
28 | '@typescript-eslint/ban-ts-comment': 'off',
29 | 'func-names': ['error', 'always'],
30 | /**
31 | * Remember to set "eslint.validate": ["javascript", "svelte"] in settings.json.
32 | */
33 | // @see https://astexplorer.net/
34 | 'no-restricted-syntax': [
35 | 'error',
36 | {
37 | selector: 'ArrowFunctionExpression',
38 | message: 'Shorthand arrow functions are not allowed.',
39 | },
40 | {
41 | selector: 'ClassDeclaration',
42 | message: 'Classes are not allowed, use object literals instead.',
43 | },
44 | ],
45 | },
46 | overrides: [
47 | {
48 | files: ['*.svelte'],
49 | parser: 'svelte-eslint-parser',
50 | parserOptions: {
51 | parser: '@typescript-eslint/parser',
52 | },
53 | },
54 | ],
55 | }
56 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /dist
5 | /.svelte-kit
6 | /package
7 | .env
8 | .env.*
9 | !.env.example
10 | vite.config.js.timestamp-*
11 | vite.config.ts.timestamp-*
12 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore files for PNPM, NPM and YARN
2 | pnpm-lock.yaml
3 | package-lock.json
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "printWidth": 80,
6 | "plugins": ["prettier-plugin-svelte"],
7 | "semi": false,
8 | "svelteSortOrder": "options-styles-scripts-markup",
9 | "svelteStrictMode": false,
10 | "svelteBracketNewLine": true,
11 | "svelteIndentScriptAndStyle": true,
12 | "useTabs": false,
13 | "pluginSearchDirs": ["."],
14 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
15 | "bracketSpacing": true,
16 | "quoteProps": "consistent"
17 | }
18 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "bun",
9 | "internalConsoleOptions": "neverOpen",
10 | "request": "attach",
11 | "name": "Attach Bun",
12 | "url": "ws://localhost:6499/",
13 | "stopOnEntry": false
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cSpell.words": ["Connectables"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Razvan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Svelte Animated Pixels
2 |
3 | This library provides an easy way to create pixelated canvas animations by just declaring maps of pixels as strings.
4 |
5 |
6 | Install with:
7 |
8 | ```sh
9 | npm i -D svelte-animated-pixels
10 | ```
11 |
12 | Then start drawing, the idea is pretty simple
13 |
14 | - `x` delimits a pixel to be drawn.
15 | - `.` delimits the background.
16 | - Any other character that's not `x` or `.` is safely ignored.
17 |
18 | ```svelte
19 |
32 |
57 |
58 |
63 | ```
64 |
65 | 
66 |
67 | > [!NOTE]
68 | > Once a frame has been drawn on a pixel map, all subsequent drawn frames MUST have the same shape, meaning both width and height MUST match.
69 |
70 | # Planned Features
71 |
72 | | Feature | Implemented |
73 | |---------|-------------|
74 | | Draw pixels using declarative maps of strings | ✅ Done - `` |
75 | | Created animations from list of maps | ✅ Done - `` |
76 | | Fine tune each pixel's color | ✖ WIP |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razshare/svelte-animated-pixels/504b1103ee105f4b6e83a0695bb3f98ec758b49d/bun.lockb
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "types": ["bun-types"],
13 | "isolatedModules": true,
14 | "lib": ["esnext", "DOM", "DOM.Iterable"],
15 | "moduleResolution": "node",
16 | "module": "esnext",
17 | "target": "esnext",
18 | "ignoreDeprecations": "5.0",
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-animated-pixels",
3 | "version": "0.1.3",
4 | "scripts": {
5 | "dev": "vite dev",
6 | "build": "vite build && npm run package",
7 | "preview": "vite preview",
8 | "package": "svelte-kit sync && svelte-package && publint",
9 | "prepublishOnly": "npm run package",
10 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
12 | "lint": "prettier --check . && eslint .",
13 | "lint:fix": "eslint --no-error-on-unmatched-pattern --fix \"./src/**/*.{js,ts,html,svelte}\"",
14 | "format": "prettier --write ."
15 | },
16 | "exports": {
17 | ".": {
18 | "types": "./dist/index.d.ts",
19 | "svelte": "./dist/index.js"
20 | }
21 | },
22 | "files": [
23 | "dist",
24 | "!dist/**/*.test.*",
25 | "!dist/**/*.spec.*"
26 | ],
27 | "peerDependencies": {
28 | "svelte": "^4.0.0"
29 | },
30 | "devDependencies": {
31 | "@sveltejs/adapter-auto": "^3.0.0",
32 | "@sveltejs/kit": "^2.0.0",
33 | "@sveltejs/package": "^2.0.0",
34 | "@sveltejs/vite-plugin-svelte": "^3.0.0",
35 | "@types/eslint": "8.56.0",
36 | "@typescript-eslint/eslint-plugin": "^6.19.1",
37 | "@typescript-eslint/parser": "^6.19.1",
38 | "autoprefixer": "^10.4.19",
39 | "bun-types": "latest",
40 | "daisyui": "3.7.3",
41 | "eslint": "^8.56.0",
42 | "eslint-config-prettier": "^9.1.0",
43 | "eslint-plugin-svelte": "^2.35.1",
44 | "install": "^0.13.0",
45 | "npm": "^10.2.5",
46 | "postcss": "8.4.28",
47 | "prettier": "^3.1.1",
48 | "prettier-plugin-svelte": "^3.1.2",
49 | "publint": "^0.1.9",
50 | "svelte": "^4.2.7",
51 | "svelte-check": "^3.6.0",
52 | "tailwindcss": "3.3.3",
53 | "tslib": "^2.4.1",
54 | "typescript": "^5.0.0",
55 | "vite": "^5.0.11"
56 | },
57 | "svelte": "./dist/index.js",
58 | "types": "./dist/index.d.ts",
59 | "type": "module"
60 | }
61 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | import tailwindcss from 'tailwindcss'
2 | import autoprefixer from 'autoprefixer'
3 |
4 | export default {
5 | plugins: [tailwindcss, autoprefixer],
6 | }
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 | }
12 |
13 | export {}
14 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/components/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razshare/svelte-animated-pixels/504b1103ee105f4b6e83a0695bb3f98ec758b49d/src/lib/components/.gitkeep
--------------------------------------------------------------------------------
/src/lib/components/Animated.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/components/PixelMap.svelte:
--------------------------------------------------------------------------------
1 |
147 |
148 |
149 |
150 |
151 |
152 |
--------------------------------------------------------------------------------
/src/lib/constants.js:
--------------------------------------------------------------------------------
1 | export const IS_BROWSER = typeof document !== 'undefined'
2 |
--------------------------------------------------------------------------------
/src/lib/createAnimation.js:
--------------------------------------------------------------------------------
1 | import { readable } from 'svelte/store'
2 | import { IS_BROWSER } from './constants'
3 |
4 | /**
5 | * @type {Map}
6 | */
7 | const tracker = new Map()
8 |
9 | /**
10 | *
11 | * @param {{id:string, frames:import('./types').Frames}} payload
12 | */
13 | function find_next_frame({ id, frames }) {
14 | const index = tracker.get(id)
15 | if (index === undefined) {
16 | tracker.set(id, 0)
17 | return frames[0] ?? ''
18 | }
19 |
20 | const next_index = (index + 1) % frames.length
21 | tracker.set(id, next_index)
22 |
23 | return frames[next_index]
24 | }
25 |
26 | /**
27 | * @typedef AnimatePayload
28 | * @property {import('./types').Frames} frames sequence of frames.
29 | * @property {string} id a unique id for the frames sequence.
30 | * @property {number} interval milliseconds between each frame.
31 | */
32 |
33 | /**
34 | * Animate a sequence of frames.
35 | * @param {AnimatePayload} payload
36 | * @returns
37 | */
38 | export function createAnimation({ frames, id, interval }) {
39 | if (!IS_BROWSER) {
40 | return readable('')
41 | }
42 | return readable('', function start(set) {
43 | set(find_next_frame({ id, frames }))
44 |
45 | const timer = setInterval(function run() {
46 | requestAnimationFrame(function run() {
47 | set(find_next_frame({ id, frames }))
48 | })
49 | }, interval)
50 |
51 | return function stop() {
52 | clearInterval(timer)
53 | }
54 | })
55 | }
56 |
--------------------------------------------------------------------------------
/src/lib/createInvalidHeightError.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param {import('$lib/types').InvalidHeightErrorPayload} received
4 | */
5 | export function createInvalidHeightError({ expected, actual }) {
6 | return new Error(
7 | `Invalid map height - expected ${expected.height} pixels, ${actual.height} received instead. Once a frame has been drawn on a pixel map, all subsequent drawn frames MUST have the same shape, meaning both width and height MUST match.
8 | Expected shape
9 | ${expected.shape}
10 |
11 | Received shape
12 | ${actual.shape}
13 | `,
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/createInvalidWidthError.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param {import('$lib/types').InvalidWidthErrorPayload} received
4 | */
5 | export function createInvalidWidthError({ expected, actual }) {
6 | return new Error(
7 | `Invalid map width - expected ${expected.width} pixels, ${actual.width} received instead. Once a frame has been drawn on a pixel map, all subsequent drawn frames MUST have the same shape, meaning both width and height MUST match.
8 | Expected shape
9 | ${expected.shape}
10 |
11 | Received shape
12 | ${actual.shape}
13 | `,
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/lib/index.js:
--------------------------------------------------------------------------------
1 | import PixelMap from './components/PixelMap.svelte'
2 | import Animated from './components/Animated.svelte'
3 | export * from './types.js'
4 | export { Animated, PixelMap }
5 |
--------------------------------------------------------------------------------
/src/lib/types.js:
--------------------------------------------------------------------------------
1 | export {}
2 |
3 | /**
4 | * @template T
5 | * @typedef {{value:T,error:false|Error}} Unsafe
6 | */
7 |
8 | /**
9 | * @typedef {Array} Frames
10 | */
11 |
12 | /**
13 | * @typedef OnResizePayload
14 | * @property {number} width
15 | * @property {number} height
16 | */
17 |
18 | /**
19 | * @callback OnResize
20 | * @param {OnResizePayload} payload
21 | */
22 |
23 | /**
24 | * @typedef InvalidHeightErrorValue
25 | * @property {number} height
26 | * @property {string} shape
27 | */
28 |
29 | /**
30 | * @typedef InvalidHeightErrorPayload
31 | * @property {InvalidHeightErrorValue} expected
32 | * @property {InvalidHeightErrorValue} actual
33 | */
34 |
35 | /**
36 | * @typedef InvalidWidthErrorValue
37 | * @property {number} width
38 | * @property {string} shape
39 | */
40 |
41 | /**
42 | * @typedef InvalidWidthErrorPayload
43 | * @property {InvalidWidthErrorValue} expected
44 | * @property {InvalidWidthErrorValue} actual
45 | */
46 |
--------------------------------------------------------------------------------
/src/lib/uuid.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef UuidPayload
3 | * @property {boolean} short If `true` the final string will 8 characters long, otherwise it will be 32 + 4 (dashes) characters long.\
4 | * For more information see https://en.wikipedia.org/wiki/Universally_unique_identifier#Textual_representation
5 | */
6 |
7 | /**
8 | * Create a [universally unique identifier](https://en.wikipedia.org/wiki/Universally_unique_identifier).
9 | * @param {UuidPayload} payload
10 | * @returns
11 | */
12 | export function uuid({ short } = { short: false }) {
13 | let dt = new Date().getTime()
14 | const BLUEPRINT = short ? 'xyxxyxyx' : 'xxxxxxxx-xxxx-yxxx-yxxx-xxxxxxxxxxxx'
15 | const RESULT = BLUEPRINT.replace(/[xy]/g, function check(c) {
16 | const r = (dt + Math.random() * 16) % 16 | 0
17 | dt = Math.floor(dt / 16)
18 | return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16)
19 | })
20 | return RESULT
21 | }
22 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
41 |
42 |
47 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/razshare/svelte-animated-pixels/504b1103ee105f4b6e83a0695bb3f98ec758b49d/static/favicon.png
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-auto'
2 |
3 | /** @type {import('@sveltejs/kit').Config} */
4 | const config = {
5 | kit: {
6 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
7 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
8 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
9 | adapter: adapter(),
10 | },
11 | }
12 |
13 | export default config
14 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import daisyui from 'daisyui'
2 |
3 | /** @type {import('daisyui').Config} */
4 | export default {
5 | content: ['./src/**/*.{html,js,ts,svelte}'],
6 | plugins: [daisyui],
7 | daisyui: {
8 | themes: true,
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite'
2 | import { defineConfig } from 'vite'
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()],
6 | })
7 |
--------------------------------------------------------------------------------