mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-09-14 21:46:35 +00:00
feat: core instruments
This commit is contained in:
+4
-2
@@ -9,7 +9,8 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.10",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"svelte": "^5.56.1",
|
"svelte": "^5.56.1",
|
||||||
"svelte-check": "^4.6.0",
|
"svelte-check": "^4.6.0",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vite": "^8.0.16"
|
"vite": "^8.0.16",
|
||||||
|
"vitest": "^4.1.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+234
@@ -29,6 +29,9 @@ importers:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.16
|
specifier: ^8.0.16
|
||||||
version: 8.2.1
|
version: 8.2.1
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.1.11
|
||||||
|
version: 4.1.11(vite@8.2.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -187,15 +190,50 @@ packages:
|
|||||||
svelte: ^5.46.4
|
svelte: ^5.46.4
|
||||||
vite: ^8.0.0-beta.7 || ^8.0.0
|
vite: ^8.0.0-beta.7 || ^8.0.0
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
|
||||||
|
|
||||||
'@types/cookie@0.6.0':
|
'@types/cookie@0.6.0':
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2':
|
||||||
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
'@types/estree@1.0.9':
|
'@types/estree@1.0.9':
|
||||||
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7':
|
'@types/trusted-types@2.0.7':
|
||||||
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.11':
|
||||||
|
resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==}
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.11':
|
||||||
|
resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==}
|
||||||
|
peerDependencies:
|
||||||
|
msw: ^2.4.9
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
msw:
|
||||||
|
optional: true
|
||||||
|
vite:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.11':
|
||||||
|
resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==}
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.11':
|
||||||
|
resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==}
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.11':
|
||||||
|
resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==}
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.11':
|
||||||
|
resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.11':
|
||||||
|
resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==}
|
||||||
|
|
||||||
acorn@8.18.0:
|
acorn@8.18.0:
|
||||||
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
@@ -205,10 +243,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
|
resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
assertion-error@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
axobject-query@4.1.0:
|
axobject-query@4.1.0:
|
||||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
chai@6.2.2:
|
||||||
|
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
@@ -217,6 +263,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
convert-source-map@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
cookie@0.6.0:
|
cookie@0.6.0:
|
||||||
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -232,6 +281,9 @@ packages:
|
|||||||
devalue@5.9.0:
|
devalue@5.9.0:
|
||||||
resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==}
|
resolution: {integrity: sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==}
|
||||||
|
|
||||||
|
es-module-lexer@2.3.2:
|
||||||
|
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
||||||
|
|
||||||
esm-env@1.2.2:
|
esm-env@1.2.2:
|
||||||
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||||
|
|
||||||
@@ -243,6 +295,13 @@ packages:
|
|||||||
'@typescript-eslint/types':
|
'@typescript-eslint/types':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
|
expect-type@1.4.0:
|
||||||
|
resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
fdir@6.5.0:
|
fdir@6.5.0:
|
||||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -364,6 +423,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==}
|
||||||
engines: {node: '>=12.20.0'}
|
engines: {node: '>=12.20.0'}
|
||||||
|
|
||||||
|
pathe@2.0.3:
|
||||||
|
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -391,6 +453,9 @@ packages:
|
|||||||
set-cookie-parser@3.1.2:
|
set-cookie-parser@3.1.2:
|
||||||
resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==}
|
resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==}
|
||||||
|
|
||||||
|
siginfo@2.0.0:
|
||||||
|
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||||
|
|
||||||
sirv@3.0.2:
|
sirv@3.0.2:
|
||||||
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
|
resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -399,6 +464,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
stackback@0.0.2:
|
||||||
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
|
std-env@4.2.0:
|
||||||
|
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
|
||||||
|
|
||||||
svelte-check@4.7.6:
|
svelte-check@4.7.6:
|
||||||
resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==}
|
resolution: {integrity: sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==}
|
||||||
engines: {node: '>= 18.0.0'}
|
engines: {node: '>= 18.0.0'}
|
||||||
@@ -411,10 +482,21 @@ packages:
|
|||||||
resolution: {integrity: sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA==}
|
resolution: {integrity: sha512-VT8kSnlEg8069w7AiCcAk3Yf5xvMnrGTagVOmU/OpOLHaHnNqXhWZCH/4EVga/bT/HtWhvE6/fHrXLErx7OnJA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
tinybench@2.9.0:
|
||||||
|
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
|
||||||
|
|
||||||
|
tinyexec@1.3.0:
|
||||||
|
resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
tinyrainbow@3.1.1:
|
||||||
|
resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==}
|
||||||
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
totalist@3.0.1:
|
totalist@3.0.1:
|
||||||
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -475,6 +557,52 @@ packages:
|
|||||||
vite:
|
vite:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
vitest@4.1.11:
|
||||||
|
resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==}
|
||||||
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@edge-runtime/vm': '*'
|
||||||
|
'@opentelemetry/api': ^1.9.0
|
||||||
|
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
|
||||||
|
'@vitest/browser-playwright': 4.1.11
|
||||||
|
'@vitest/browser-preview': 4.1.11
|
||||||
|
'@vitest/browser-webdriverio': 4.1.11
|
||||||
|
'@vitest/coverage-istanbul': 4.1.11
|
||||||
|
'@vitest/coverage-v8': 4.1.11
|
||||||
|
'@vitest/ui': 4.1.11
|
||||||
|
happy-dom: '*'
|
||||||
|
jsdom: '*'
|
||||||
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@edge-runtime/vm':
|
||||||
|
optional: true
|
||||||
|
'@opentelemetry/api':
|
||||||
|
optional: true
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-playwright':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-preview':
|
||||||
|
optional: true
|
||||||
|
'@vitest/browser-webdriverio':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-istanbul':
|
||||||
|
optional: true
|
||||||
|
'@vitest/coverage-v8':
|
||||||
|
optional: true
|
||||||
|
'@vitest/ui':
|
||||||
|
optional: true
|
||||||
|
happy-dom:
|
||||||
|
optional: true
|
||||||
|
jsdom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
zimmerframe@1.1.4:
|
zimmerframe@1.1.4:
|
||||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||||
|
|
||||||
@@ -588,24 +716,78 @@ snapshots:
|
|||||||
vite: 8.2.1
|
vite: 8.2.1
|
||||||
vitefu: 1.1.3(vite@8.2.1)
|
vitefu: 1.1.3(vite@8.2.1)
|
||||||
|
|
||||||
|
'@types/chai@5.2.3':
|
||||||
|
dependencies:
|
||||||
|
'@types/deep-eql': 4.0.2
|
||||||
|
assertion-error: 2.0.1
|
||||||
|
|
||||||
'@types/cookie@0.6.0': {}
|
'@types/cookie@0.6.0': {}
|
||||||
|
|
||||||
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
'@types/estree@1.0.9': {}
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
'@types/trusted-types@2.0.7': {}
|
'@types/trusted-types@2.0.7': {}
|
||||||
|
|
||||||
|
'@vitest/expect@4.1.11':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
'@types/chai': 5.2.3
|
||||||
|
'@vitest/spy': 4.1.11
|
||||||
|
'@vitest/utils': 4.1.11
|
||||||
|
chai: 6.2.2
|
||||||
|
tinyrainbow: 3.1.1
|
||||||
|
|
||||||
|
'@vitest/mocker@4.1.11(vite@8.2.1)':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/spy': 4.1.11
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
magic-string: 0.30.21
|
||||||
|
optionalDependencies:
|
||||||
|
vite: 8.2.1
|
||||||
|
|
||||||
|
'@vitest/pretty-format@4.1.11':
|
||||||
|
dependencies:
|
||||||
|
tinyrainbow: 3.1.1
|
||||||
|
|
||||||
|
'@vitest/runner@4.1.11':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/utils': 4.1.11
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/snapshot@4.1.11':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.11
|
||||||
|
'@vitest/utils': 4.1.11
|
||||||
|
magic-string: 0.30.21
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
'@vitest/spy@4.1.11': {}
|
||||||
|
|
||||||
|
'@vitest/utils@4.1.11':
|
||||||
|
dependencies:
|
||||||
|
'@vitest/pretty-format': 4.1.11
|
||||||
|
convert-source-map: 2.0.0
|
||||||
|
tinyrainbow: 3.1.1
|
||||||
|
|
||||||
acorn@8.18.0: {}
|
acorn@8.18.0: {}
|
||||||
|
|
||||||
aria-query@5.3.1: {}
|
aria-query@5.3.1: {}
|
||||||
|
|
||||||
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
axobject-query@4.1.0: {}
|
axobject-query@4.1.0: {}
|
||||||
|
|
||||||
|
chai@6.2.2: {}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
cookie@0.6.0: {}
|
cookie@0.6.0: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
@@ -614,12 +796,20 @@ snapshots:
|
|||||||
|
|
||||||
devalue@5.9.0: {}
|
devalue@5.9.0: {}
|
||||||
|
|
||||||
|
es-module-lexer@2.3.2: {}
|
||||||
|
|
||||||
esm-env@1.2.2: {}
|
esm-env@1.2.2: {}
|
||||||
|
|
||||||
esrap@2.3.5:
|
esrap@2.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
|
expect-type@1.4.0: {}
|
||||||
|
|
||||||
fdir@6.5.0(picomatch@4.0.5):
|
fdir@6.5.0(picomatch@4.0.5):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.5
|
picomatch: 4.0.5
|
||||||
@@ -700,6 +890,8 @@ snapshots:
|
|||||||
|
|
||||||
obug@2.1.4: {}
|
obug@2.1.4: {}
|
||||||
|
|
||||||
|
pathe@2.0.3: {}
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.5: {}
|
picomatch@4.0.5: {}
|
||||||
@@ -738,6 +930,8 @@ snapshots:
|
|||||||
|
|
||||||
set-cookie-parser@3.1.2: {}
|
set-cookie-parser@3.1.2: {}
|
||||||
|
|
||||||
|
siginfo@2.0.0: {}
|
||||||
|
|
||||||
sirv@3.0.2:
|
sirv@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@polka/url': 1.0.0-next.29
|
'@polka/url': 1.0.0-next.29
|
||||||
@@ -746,6 +940,10 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
|
std-env@4.2.0: {}
|
||||||
|
|
||||||
svelte-check@4.7.6(picomatch@4.0.5)(svelte@5.56.9)(typescript@6.0.3):
|
svelte-check@4.7.6(picomatch@4.0.5)(svelte@5.56.9)(typescript@6.0.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.31
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
@@ -780,11 +978,17 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@typescript-eslint/types'
|
- '@typescript-eslint/types'
|
||||||
|
|
||||||
|
tinybench@2.9.0: {}
|
||||||
|
|
||||||
|
tinyexec@1.3.0: {}
|
||||||
|
|
||||||
tinyglobby@0.2.17:
|
tinyglobby@0.2.17:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.5.0(picomatch@4.0.5)
|
fdir: 6.5.0(picomatch@4.0.5)
|
||||||
picomatch: 4.0.5
|
picomatch: 4.0.5
|
||||||
|
|
||||||
|
tinyrainbow@3.1.1: {}
|
||||||
|
|
||||||
totalist@3.0.1: {}
|
totalist@3.0.1: {}
|
||||||
|
|
||||||
typescript@6.0.3: {}
|
typescript@6.0.3: {}
|
||||||
@@ -803,4 +1007,34 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.2.1
|
vite: 8.2.1
|
||||||
|
|
||||||
|
vitest@4.1.11(vite@8.2.1):
|
||||||
|
dependencies:
|
||||||
|
'@vitest/expect': 4.1.11
|
||||||
|
'@vitest/mocker': 4.1.11(vite@8.2.1)
|
||||||
|
'@vitest/pretty-format': 4.1.11
|
||||||
|
'@vitest/runner': 4.1.11
|
||||||
|
'@vitest/snapshot': 4.1.11
|
||||||
|
'@vitest/spy': 4.1.11
|
||||||
|
'@vitest/utils': 4.1.11
|
||||||
|
es-module-lexer: 2.3.2
|
||||||
|
expect-type: 1.4.0
|
||||||
|
magic-string: 0.30.21
|
||||||
|
obug: 2.1.4
|
||||||
|
pathe: 2.0.3
|
||||||
|
picomatch: 4.0.5
|
||||||
|
std-env: 4.2.0
|
||||||
|
tinybench: 2.9.0
|
||||||
|
tinyexec: 1.3.0
|
||||||
|
tinyglobby: 0.2.17
|
||||||
|
tinyrainbow: 3.1.1
|
||||||
|
vite: 8.2.1
|
||||||
|
why-is-node-running: 2.3.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- msw
|
||||||
|
|
||||||
|
why-is-node-running@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
siginfo: 2.0.0
|
||||||
|
stackback: 0.0.2
|
||||||
|
|
||||||
zimmerframe@1.1.4: {}
|
zimmerframe@1.1.4: {}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { parseHex, removeColorToAlpha } from './alpha';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('removeColorToAlpha', () => {
|
||||||
|
const fixture = () =>
|
||||||
|
makeImage(2, 1, [
|
||||||
|
[255, 255, 255, 255],
|
||||||
|
[255, 0, 0, 255]
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('обнуляет альфу только у точного совпадения при tolerance=0', () => {
|
||||||
|
const out = removeColorToAlpha(fixture(), '#ff0000', 0);
|
||||||
|
expect(out.data[3]).toBe(255);
|
||||||
|
expect(out.data[7]).toBe(0);
|
||||||
|
expect(out.data[4]).toBe(255);
|
||||||
|
expect(out.data[5]).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('поддерживает короткую форму и регистр hex', () => {
|
||||||
|
expect([...removeColorToAlpha(fixture(), '#f00', 0).data.slice(4)]).toEqual([
|
||||||
|
255, 0, 0, 0
|
||||||
|
]);
|
||||||
|
expect([...removeColorToAlpha(fixture(), '#FF0000', 0).data.slice(4)]).toEqual([
|
||||||
|
255, 0, 0, 0
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tolerance 100% удаляет весь диапазон расстояний', () => {
|
||||||
|
const out = removeColorToAlpha(fixture(), '#000000', 100);
|
||||||
|
expect(out.data[3]).toBe(0);
|
||||||
|
expect(out.data[7]).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('промежуточный tolerance различает близкие и далёкие цвета', () => {
|
||||||
|
const img = makeImage(2, 1, [
|
||||||
|
[0, 0, 0, 255],
|
||||||
|
[128, 128, 128, 255]
|
||||||
|
]);
|
||||||
|
const kept = removeColorToAlpha(img, '#000000', 40);
|
||||||
|
const removed = removeColorToAlpha(img, '#000000', 60);
|
||||||
|
expect(kept.data[3]).toBe(0);
|
||||||
|
expect(kept.data[7]).toBe(255);
|
||||||
|
expect(removed.data[3]).toBe(0);
|
||||||
|
expect(removed.data[7]).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('не мутирует вход', () => {
|
||||||
|
const img = fixture();
|
||||||
|
removeColorToAlpha(img, '#ffffff', 100);
|
||||||
|
expect([...img.data]).toEqual([
|
||||||
|
255, 255, 255, 255,
|
||||||
|
255, 0, 0, 255
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseHex', () => {
|
||||||
|
it('разбирает #rrggbb, rrggbb, #rgb', () => {
|
||||||
|
expect(parseHex('#ff8040')).toEqual([255, 128, 64]);
|
||||||
|
expect(parseHex('ff8040')).toEqual([255, 128, 64]);
|
||||||
|
expect(parseHex('#F80')).toEqual([255, 136, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(['zzz', '12345', '##ff', ''])('бросает ошибку на "%s"', (bad) => {
|
||||||
|
expect(() => parseHex(bad)).toThrow(/Некорректный HEX/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { PixelImage } from './types';
|
||||||
|
|
||||||
|
const MAX_COLOR_DISTANCE = Math.sqrt(3 * 255 * 255);
|
||||||
|
|
||||||
|
export function removeColorToAlpha(
|
||||||
|
img: PixelImage,
|
||||||
|
hex: string,
|
||||||
|
tolerancePercent = 0
|
||||||
|
): PixelImage {
|
||||||
|
const [targetR, targetG, targetB] = parseHex(hex);
|
||||||
|
const tolerance = (clamp(tolerancePercent, 0, 100) / 100) * MAX_COLOR_DISTANCE;
|
||||||
|
const thresholdSq = tolerance * tolerance;
|
||||||
|
const out: PixelImage = { width: img.width, height: img.height, data: img.data.slice() };
|
||||||
|
for (let i = 0; i < out.data.length; i += 4) {
|
||||||
|
const dr = out.data[i] - targetR;
|
||||||
|
const dg = out.data[i + 1] - targetG;
|
||||||
|
const db = out.data[i + 2] - targetB;
|
||||||
|
if (dr * dr + dg * dg + db * db <= thresholdSq) {
|
||||||
|
out.data[i + 3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseHex(hex: string): [number, number, number] {
|
||||||
|
const match = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`Некорректный HEX-цвет: "${hex}"`);
|
||||||
|
}
|
||||||
|
const digits = match[1];
|
||||||
|
if (digits.length === 3) {
|
||||||
|
return [
|
||||||
|
parseInt(digits[0] + digits[0], 16),
|
||||||
|
parseInt(digits[1] + digits[1], 16),
|
||||||
|
parseInt(digits[2] + digits[2], 16)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
parseInt(digits.slice(0, 2), 16),
|
||||||
|
parseInt(digits.slice(2, 4), 16),
|
||||||
|
parseInt(digits.slice(4, 6), 16)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { imageInfo } from './analyze';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('imageInfo', () => {
|
||||||
|
it('находит полупрозрачные пиксели и считает уникальные RGBA-цвета', () => {
|
||||||
|
const info = imageInfo(
|
||||||
|
makeImage(2, 2, [
|
||||||
|
[0, 0, 0, 255],
|
||||||
|
[0, 0, 0, 255],
|
||||||
|
[255, 0, 0, 128],
|
||||||
|
[255, 0, 0, 128]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(info).toEqual({ width: 2, height: 2, hasAlpha: true, colorCount: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('полностью непрозрачное изображение — hasAlpha false', () => {
|
||||||
|
const info = imageInfo(
|
||||||
|
makeImage(2, 1, [
|
||||||
|
[10, 20, 30, 255],
|
||||||
|
[40, 50, 60, 255]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(info.hasAlpha).toBe(false);
|
||||||
|
expect(info.colorCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('разная альфа означает разные цвета', () => {
|
||||||
|
const info = imageInfo(
|
||||||
|
makeImage(2, 1, [
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[255, 0, 0, 128]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
expect(info.hasAlpha).toBe(true);
|
||||||
|
expect(info.colorCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('возвращает корректные размеры неквадрата', () => {
|
||||||
|
const info = imageInfo(makeImage(5, 3, new Array(15).fill([1, 2, 3, 4])));
|
||||||
|
expect(info.width).toBe(5);
|
||||||
|
expect(info.height).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { PixelImage } from './types';
|
||||||
|
|
||||||
|
export type ImageInfo = {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
hasAlpha: boolean;
|
||||||
|
colorCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function imageInfo(img: PixelImage): ImageInfo {
|
||||||
|
let hasAlpha = false;
|
||||||
|
const seen = new Set<number>();
|
||||||
|
for (let i = 0; i < img.data.length; i += 4) {
|
||||||
|
if (!hasAlpha && img.data[i + 3] !== 255) {
|
||||||
|
hasAlpha = true;
|
||||||
|
}
|
||||||
|
const key =
|
||||||
|
((img.data[i] << 24) |
|
||||||
|
(img.data[i + 1] << 16) |
|
||||||
|
(img.data[i + 2] << 8) |
|
||||||
|
img.data[i + 3]) >>>
|
||||||
|
0;
|
||||||
|
seen.add(key);
|
||||||
|
}
|
||||||
|
return { width: img.width, height: img.height, hasAlpha, colorCount: seen.size };
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { brightnessContrast, grayscale, invert } from './color';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
describe('grayscale', () => {
|
||||||
|
it('считает luma по весам BT.601 с округлением', () => {
|
||||||
|
const out = grayscale(
|
||||||
|
makeImage(3, 1, [
|
||||||
|
[255, 0, 0, 255],
|
||||||
|
[0, 255, 0, 255],
|
||||||
|
[0, 0, 255, 255]
|
||||||
|
])
|
||||||
|
);
|
||||||
|
const rgb = [...out.data].reduce<number[]>((acc, v, i) => {
|
||||||
|
if (i % 4 === 0) acc.push(v);
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
expect(rgb).toEqual([76, 150, 29]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('сохраняет альфу и не мутирует вход', () => {
|
||||||
|
const img = makeImage(1, 1, [[10, 20, 30, 200]]);
|
||||||
|
const out = grayscale(img);
|
||||||
|
expect([...out.data]).toEqual([18, 18, 18, 200]);
|
||||||
|
expect([...img.data]).toEqual([10, 20, 30, 200]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('invert', () => {
|
||||||
|
it('инвертирует RGB, не трогая альфу', () => {
|
||||||
|
const out = invert(makeImage(1, 1, [[10, 200, 30, 7]]));
|
||||||
|
expect([...out.data]).toEqual([245, 55, 225, 7]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('brightnessContrast', () => {
|
||||||
|
const pixel = (r: number) => makeImage(1, 1, [[r, r, r, 255]]);
|
||||||
|
const red = (out: number[]) => [out[0], out[1], out[2]];
|
||||||
|
|
||||||
|
it('b=0, c=0 — тождественное преобразование', () => {
|
||||||
|
const out = brightnessContrast(pixel(77), 0, 0);
|
||||||
|
expect(red([...out.data])).toEqual([77, 77, 77]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('brightness +100 насыщает всё в белый', () => {
|
||||||
|
const out = brightnessContrast(pixel(10), 100, 0);
|
||||||
|
expect(red([...out.data])).toEqual([255, 255, 255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('brightness -100 заливает чёрным', () => {
|
||||||
|
const out = brightnessContrast(pixel(240), -100, 0);
|
||||||
|
expect(red([...out.data])).toEqual([0, 0, 0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('contrast -100 сводит всё к серому 128', () => {
|
||||||
|
const out = brightnessContrast(pixel(30), 0, -100);
|
||||||
|
expect(red([...out.data])).toEqual([128, 128, 128]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('параметры вне диапазона клампятся', () => {
|
||||||
|
const out = brightnessContrast(pixel(10), 150, 0);
|
||||||
|
expect(red([...out.data])).toEqual([255, 255, 255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('альфа не меняется', () => {
|
||||||
|
const out = brightnessContrast(makeImage(1, 1, [[10, 20, 30, 64]]), 50, 50);
|
||||||
|
expect(out.data[3]).toBe(64);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { createPixelImage, type PixelImage } from './types';
|
||||||
|
|
||||||
|
export function grayscale(img: PixelImage): PixelImage {
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < img.data.length; i += 4) {
|
||||||
|
const luma = 0.299 * img.data[i] + 0.587 * img.data[i + 1] + 0.114 * img.data[i + 2];
|
||||||
|
out.data[i] = luma;
|
||||||
|
out.data[i + 1] = luma;
|
||||||
|
out.data[i + 2] = luma;
|
||||||
|
out.data[i + 3] = img.data[i + 3];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function invert(img: PixelImage): PixelImage {
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < img.data.length; i += 4) {
|
||||||
|
out.data[i] = 255 - img.data[i];
|
||||||
|
out.data[i + 1] = 255 - img.data[i + 1];
|
||||||
|
out.data[i + 2] = 255 - img.data[i + 2];
|
||||||
|
out.data[i + 3] = img.data[i + 3];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function brightnessContrast(
|
||||||
|
img: PixelImage,
|
||||||
|
brightness: number,
|
||||||
|
contrast: number
|
||||||
|
): PixelImage {
|
||||||
|
const offset = (clamp(brightness, -100, 100) / 100) * 255;
|
||||||
|
const c = (clamp(contrast, -100, 100) / 100) * 255;
|
||||||
|
const factor = (259 * (c + 255)) / (255 * (259 - c));
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let i = 0; i < img.data.length; i += 4) {
|
||||||
|
for (let ch = 0; ch < 3; ch++) {
|
||||||
|
const v = factor * (img.data[i + ch] + offset - 128) + 128;
|
||||||
|
out.data[i + ch] = v;
|
||||||
|
}
|
||||||
|
out.data[i + 3] = img.data[i + 3];
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { crop, flip, resize, rotate90 } from './geometry';
|
||||||
|
import { makeImage } from './test-helpers';
|
||||||
|
|
||||||
|
const square = () =>
|
||||||
|
makeImage(2, 2, [
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[2, 2, 2, 2],
|
||||||
|
[3, 3, 3, 3],
|
||||||
|
[4, 4, 4, 4]
|
||||||
|
]);
|
||||||
|
|
||||||
|
describe('flip', () => {
|
||||||
|
it('отражает по горизонтали (зеркало слева-направо)', () => {
|
||||||
|
const out = flip(square(), 'horizontal');
|
||||||
|
expect(out.width).toBe(2);
|
||||||
|
expect(out.height).toBe(2);
|
||||||
|
expect([...out.data]).toEqual([
|
||||||
|
2, 2, 2, 2,
|
||||||
|
1, 1, 1, 1,
|
||||||
|
4, 4, 4, 4,
|
||||||
|
3, 3, 3, 3
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('отражает по вертикали (сверху-вниз)', () => {
|
||||||
|
const out = flip(square(), 'vertical');
|
||||||
|
expect([...out.data]).toEqual([
|
||||||
|
3, 3, 3, 3,
|
||||||
|
4, 4, 4, 4,
|
||||||
|
1, 1, 1, 1,
|
||||||
|
2, 2, 2, 2
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('не мутирует вход', () => {
|
||||||
|
const img = square();
|
||||||
|
flip(img, 'horizontal');
|
||||||
|
expect([...img.data]).toEqual([1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rotate90', () => {
|
||||||
|
it('поворачивает неквадрат 2x3 на 90° по часовой', () => {
|
||||||
|
const rect = makeImage(2, 3, [
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[2, 2, 2, 2],
|
||||||
|
[3, 3, 3, 3],
|
||||||
|
[4, 4, 4, 4],
|
||||||
|
[5, 5, 5, 5],
|
||||||
|
[6, 6, 6, 6]
|
||||||
|
]);
|
||||||
|
const out = rotate90(rect, 1);
|
||||||
|
expect(out.width).toBe(3);
|
||||||
|
expect(out.height).toBe(2);
|
||||||
|
expect([...out.data]).toEqual([
|
||||||
|
5, 5, 5, 5,
|
||||||
|
3, 3, 3, 3,
|
||||||
|
1, 1, 1, 1,
|
||||||
|
6, 6, 6, 6,
|
||||||
|
4, 4, 4, 4,
|
||||||
|
2, 2, 2, 2
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('turns=0 возвращает копию без изменений', () => {
|
||||||
|
const img = square();
|
||||||
|
const out = rotate90(img, 0);
|
||||||
|
expect(out).not.toBe(img);
|
||||||
|
expect([...out.data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('нормализует turns: 5 ≡ 1, -3 ≡ 1, 4 ≡ 0', () => {
|
||||||
|
const once = [...rotate90(square(), 1).data];
|
||||||
|
expect([...rotate90(square(), 5).data]).toEqual(once);
|
||||||
|
expect([...rotate90(square(), -3).data]).toEqual(once);
|
||||||
|
expect([...rotate90(square(), 4).data]).toEqual([...square().data]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('turns=2 на квадрате равно двойному отражению', () => {
|
||||||
|
const out = rotate90(square(), 2);
|
||||||
|
expect([...out.data]).toEqual([
|
||||||
|
4, 4, 4, 4,
|
||||||
|
3, 3, 3, 3,
|
||||||
|
2, 2, 2, 2,
|
||||||
|
1, 1, 1, 1
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('crop', () => {
|
||||||
|
const grid = () =>
|
||||||
|
makeImage(3, 3, [
|
||||||
|
[1, 1, 1, 1],
|
||||||
|
[2, 2, 2, 2],
|
||||||
|
[3, 3, 3, 3],
|
||||||
|
[4, 4, 4, 4],
|
||||||
|
[5, 5, 5, 5],
|
||||||
|
[6, 6, 6, 6],
|
||||||
|
[7, 7, 7, 7],
|
||||||
|
[8, 8, 8, 8],
|
||||||
|
[9, 9, 9, 9]
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('вырезает центральную область 2x2 из 3x3', () => {
|
||||||
|
const out = crop(grid(), 1, 1, 2, 2);
|
||||||
|
expect(out.width).toBe(2);
|
||||||
|
expect(out.height).toBe(2);
|
||||||
|
expect([...out.data]).toEqual([
|
||||||
|
5, 5, 5, 5,
|
||||||
|
6, 6, 6, 6,
|
||||||
|
8, 8, 8, 8,
|
||||||
|
9, 9, 9, 9
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('усекает область, выходящую за границы', () => {
|
||||||
|
const out = crop(grid(), -1, -1, 2, 2);
|
||||||
|
expect(out.width).toBe(1);
|
||||||
|
expect(out.height).toBe(1);
|
||||||
|
expect([...out.data]).toEqual([1, 1, 1, 1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('бросает RangeError для области вне изображения', () => {
|
||||||
|
expect(() => crop(grid(), 5, 5, 2, 2)).toThrow(RangeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resize', () => {
|
||||||
|
const twoByTwo = () =>
|
||||||
|
makeImage(2, 2, [
|
||||||
|
[0, 0, 0, 255],
|
||||||
|
[100, 100, 100, 255],
|
||||||
|
[200, 200, 200, 255],
|
||||||
|
[255, 255, 255, 255]
|
||||||
|
]);
|
||||||
|
|
||||||
|
it('совпадает с входом при тех же размерах', () => {
|
||||||
|
const img = twoByTwo();
|
||||||
|
expect([...resize(img, 2, 2).data]).toEqual([...img.data]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('апскейл 2x2 -> 4x4 билинейно интерполирует', () => {
|
||||||
|
const out = resize(twoByTwo(), 4, 4);
|
||||||
|
expect(out.width).toBe(4);
|
||||||
|
expect(out.height).toBe(4);
|
||||||
|
const red = [...out.data].filter((_, i) => i % 4 === 0);
|
||||||
|
expect(red.slice(0, 4)).toEqual([0, 25, 75, 100]);
|
||||||
|
expect(red.slice(4, 8)).toEqual([50, 72, 117, 139]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('бросает RangeError на некорректные размеры', () => {
|
||||||
|
const img = twoByTwo();
|
||||||
|
expect(() => resize(img, 0, 10)).toThrow(RangeError);
|
||||||
|
expect(() => resize(img, 10.5, 10)).toThrow(RangeError);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { clonePixelImage, createPixelImage, type PixelImage } from './types';
|
||||||
|
|
||||||
|
export type FlipAxis = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
export function flip(img: PixelImage, axis: FlipAxis): PixelImage {
|
||||||
|
const out = createPixelImage(img.width, img.height);
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
for (let x = 0; x < img.width; x++) {
|
||||||
|
const sx = axis === 'horizontal' ? img.width - 1 - x : x;
|
||||||
|
const sy = axis === 'vertical' ? img.height - 1 - y : y;
|
||||||
|
copyPixel(img, sx, sy, out, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rotate90(img: PixelImage, turns: number): PixelImage {
|
||||||
|
const t = ((Math.trunc(turns) % 4) + 4) % 4;
|
||||||
|
if (t === 0) return clonePixelImage(img);
|
||||||
|
let current = img;
|
||||||
|
for (let i = 0; i < t; i++) {
|
||||||
|
current = rotateClockwise(current);
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateClockwise(img: PixelImage): PixelImage {
|
||||||
|
const out = createPixelImage(img.height, img.width);
|
||||||
|
for (let y = 0; y < img.height; y++) {
|
||||||
|
for (let x = 0; x < img.width; x++) {
|
||||||
|
copyPixel(img, x, y, out, img.height - 1 - y, x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function crop(
|
||||||
|
img: PixelImage,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): PixelImage {
|
||||||
|
const sx = clampInt(x, 0, img.width);
|
||||||
|
const sy = clampInt(y, 0, img.height);
|
||||||
|
const ex = clampInt(x + width, sx, img.width);
|
||||||
|
const ey = clampInt(y + height, sy, img.height);
|
||||||
|
const w = ex - sx;
|
||||||
|
const h = ey - sy;
|
||||||
|
if (w <= 0 || h <= 0) {
|
||||||
|
throw new RangeError('Область обрезки пуста: она целиком вне изображения');
|
||||||
|
}
|
||||||
|
const out = createPixelImage(w, h);
|
||||||
|
for (let row = 0; row < h; row++) {
|
||||||
|
const si = (sy + row) * img.width * 4 + sx * 4;
|
||||||
|
out.data.set(img.data.subarray(si, si + w * 4), row * w * 4);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resize(img: PixelImage, width: number, height: number): PixelImage {
|
||||||
|
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
||||||
|
throw new RangeError('Размеры должны быть целыми числами >= 1');
|
||||||
|
}
|
||||||
|
const out = createPixelImage(width, height);
|
||||||
|
const xr = img.width / width;
|
||||||
|
const yr = img.height / height;
|
||||||
|
const maxX = img.width - 1;
|
||||||
|
const maxY = img.height - 1;
|
||||||
|
for (let dy = 0; dy < height; dy++) {
|
||||||
|
const fy = Math.min(Math.max((dy + 0.5) * yr - 0.5, 0), maxY);
|
||||||
|
const y0 = Math.floor(fy);
|
||||||
|
const ty = fy - y0;
|
||||||
|
const y1 = Math.min(y0 + 1, maxY);
|
||||||
|
for (let dx = 0; dx < width; dx++) {
|
||||||
|
const fx = Math.min(Math.max((dx + 0.5) * xr - 0.5, 0), maxX);
|
||||||
|
const x0 = Math.floor(fx);
|
||||||
|
const tx = fx - x0;
|
||||||
|
const x1 = Math.min(x0 + 1, maxX);
|
||||||
|
const di = (dy * width + dx) * 4;
|
||||||
|
for (let ch = 0; ch < 4; ch++) {
|
||||||
|
const p00 = img.data[(y0 * img.width + x0) * 4 + ch];
|
||||||
|
const p10 = img.data[(y0 * img.width + x1) * 4 + ch];
|
||||||
|
const p01 = img.data[(y1 * img.width + x0) * 4 + ch];
|
||||||
|
const p11 = img.data[(y1 * img.width + x1) * 4 + ch];
|
||||||
|
const top = (1 - tx) * p00 + tx * p10;
|
||||||
|
const bottom = (1 - tx) * p01 + tx * p11;
|
||||||
|
out.data[di + ch] = (1 - ty) * top + ty * bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPixel(src: PixelImage, sx: number, sy: number, dst: PixelImage, dx: number, dy: number): void {
|
||||||
|
const si = (sy * src.width + sx) * 4;
|
||||||
|
const di = (dy * dst.width + dx) * 4;
|
||||||
|
dst.data[di] = src.data[si];
|
||||||
|
dst.data[di + 1] = src.data[si + 1];
|
||||||
|
dst.data[di + 2] = src.data[si + 2];
|
||||||
|
dst.data[di + 3] = src.data[si + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampInt(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { expect } from 'vitest';
|
||||||
|
import type { PixelImage } from './types';
|
||||||
|
|
||||||
|
export function makeImage(width: number, height: number, pixels: number[][]): PixelImage {
|
||||||
|
const data = new Uint8ClampedArray(pixels.flat());
|
||||||
|
if (data.length !== width * height * 4) {
|
||||||
|
throw new Error(`Fixture mismatch: ${data.length} байт на ${width}x${height}`);
|
||||||
|
}
|
||||||
|
return { width, height, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function expectImageEqual(actual: PixelImage, expectedPixels: number[][]): void {
|
||||||
|
expect([...actual.data]).toEqual(expectedPixels.flat());
|
||||||
|
}
|
||||||
@@ -11,3 +11,7 @@ export function createPixelImage(width: number, height: number): PixelImage {
|
|||||||
data: new Uint8ClampedArray(width * height * 4)
|
data: new Uint8ClampedArray(width * height * 4)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clonePixelImage(img: PixelImage): PixelImage {
|
||||||
|
return { width: img.width, height: img.height, data: img.data.slice() };
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
include: ['src/**/*.test.ts'],
|
||||||
|
environment: 'node'
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user