├── .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 |
59 | 60 | 61 | 62 |
63 | ``` 64 | 65 | ![Peek 2024-04-28 23-54](https://github.com/tncrazvan/svelte-animated-pixels/assets/6891346/6b83adba-1805-4c8f-9f1f-813707629550) 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 |
43 | 44 | 45 | 46 |
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 | --------------------------------------------------------------------------------