hell ya bby look at that life bar

main
Mari 1 year ago
parent 52f05d96fd
commit 04d373b6b4
  1. 6
      .idea/vcs.xml
  2. 48
      README.md
  3. 290
      package-lock.json
  4. 4
      package.json
  5. 115
      src/App.tsx
  6. 181
      src/CharacterStatus.css
  7. 232
      src/CharacterStatus.tsx
  8. 131
      src/SpringyValueHook.ts
  9. 20
      src/fabula-points.svg
  10. 1
      src/logo.svg
  11. 81
      src/resource_bar.tsx
  12. 3
      src/type_check.ts
  13. 20
      src/ultima-points.svg

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -1,46 +1,4 @@
# Getting Started with Create React App
# Image Sources
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
* src/fabula-points.svg: https://game-icons.net/1x1/lorc/star-swirl.html
* src/ultima-points.svg: https://game-icons.net/1x1/lorc/evil-moon.html

290
package-lock.json generated

@ -8,6 +8,7 @@
"name": "fabula-ultima-react",
"version": "0.1.0",
"dependencies": {
"@react-spring/web": "^9.7.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@ -15,7 +16,10 @@
"@types/node": "^16.18.23",
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"bootstrap": "^5.2.3",
"csstype": "^3.1.2",
"react": "^18.2.0",
"react-bootstrap": "^2.7.2",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
@ -3081,6 +3085,135 @@
}
}
},
"node_modules/@popperjs/core": {
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.6.0.tgz",
"integrity": "sha512-OFiYQdv+Yk7AO7IsQu/fAEPijbeTwrrEYvdNoJ3sblBBedD5j5fBTNWrUPNVlwC4XWWnWTCMaRIVsJujsFiWXg==",
"dependencies": {
"@swc/helpers": "^0.4.14"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0"
}
},
"node_modules/@react-spring/animated": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.2.tgz",
"integrity": "sha512-ipvleJ99ipqlnHkz5qhSsgf/ny5aW0ZG8Q+/2Oj9cI7LCc7COdnrSO6V/v8MAX3JOoQNzfz6dye2s5Pt5jGaIA==",
"dependencies": {
"@react-spring/shared": "~9.7.2",
"@react-spring/types": "~9.7.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/core": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.2.tgz",
"integrity": "sha512-fF512edZT/gKVCA90ZRxfw1DmELeVwiL4OC2J6bMUlNr707C0h4QRoec6DjzG27uLX2MvS1CEatf9KRjwZR9/w==",
"dependencies": {
"@react-spring/animated": "~9.7.2",
"@react-spring/rafz": "~9.7.2",
"@react-spring/shared": "~9.7.2",
"@react-spring/types": "~9.7.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-spring/donate"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/rafz": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.2.tgz",
"integrity": "sha512-kDWMYDQto3+flkrX3vy6DU/l9pxQ4TVW91DglQEc11iDc7shF4+WVDRJvOVLX+xoMP7zyag1dMvlIgvQ+dvA/A=="
},
"node_modules/@react-spring/shared": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.2.tgz",
"integrity": "sha512-6U9qkno+9DxlH5nSltnPs+kU6tYKf0bPLURX2te13aGel8YqgcpFYp5Av8DcN2x3sukinAsmzHUS/FRsdZMMBA==",
"dependencies": {
"@react-spring/rafz": "~9.7.2",
"@react-spring/types": "~9.7.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@react-spring/types": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.2.tgz",
"integrity": "sha512-GEflx2Ex/TKVMHq5g5MxQDNNPNhqg+4Db9m7+vGTm8ttZiyga7YQUF24shgRNebKIjahqCuei16SZga8h1pe4g=="
},
"node_modules/@react-spring/web": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.2.tgz",
"integrity": "sha512-7qNc7/5KShu2D05x7o2Ols2nUE7mCKfKLaY2Ix70xPMfTle1sZisoQMBFgV9w/fSLZlHZHV9P0uWJqEXQnbV4Q==",
"dependencies": {
"@react-spring/animated": "~9.7.2",
"@react-spring/core": "~9.7.2",
"@react-spring/shared": "~9.7.2",
"@react-spring/types": "~9.7.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/@restart/hooks": {
"version": "0.4.9",
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.9.tgz",
"integrity": "sha512-3BekqcwB6Umeya+16XPooARn4qEPW6vNvwYnlofIYe6h9qG1/VeD7UvShCWx11eFz5ELYmwIEshz+MkPX3wjcQ==",
"dependencies": {
"dequal": "^2.0.2"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@restart/ui": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.2.tgz",
"integrity": "sha512-hcYs8PwpmHEtwjihLVn2Jr89yrYajfhxN5HtTq3HA9U3+feg1SC3swBM8/qibMTCFsXWToEEtzJMV+LWE+Qjpg==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@popperjs/core": "^2.11.6",
"@react-aria/ssr": "^3.5.0",
"@restart/hooks": "^0.4.9",
"@types/warning": "^3.0.0",
"dequal": "^2.0.3",
"dom-helpers": "^5.2.0",
"uncontrollable": "^8.0.0",
"warning": "^4.0.3"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/@restart/ui/node_modules/uncontrollable": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.0.tgz",
"integrity": "sha512-a954G/0JyXoZdpt0YIzTfoQyWtRS1VvygOBsHttCtZL8jDTKd6vQlUo811y46XnWoXIqQ36QKi3cSEdPuFADkA==",
"dependencies": {
"@types/react": ">=18.0.28"
},
"peerDependencies": {
"react": ">=17.0.0"
}
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -3399,6 +3532,14 @@
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@swc/helpers": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.14.tgz",
"integrity": "sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==",
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@testing-library/dom": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.2.0.tgz",
@ -3939,6 +4080,14 @@
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -4005,6 +4154,11 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz",
"integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g=="
},
"node_modules/@types/warning": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
"integrity": "sha512-t/Tvs5qR47OLOr+4E9ckN8AmP2Tf16gWq+/qA4iUGS/OOyHVO8wv2vjJuX8SNOUTJyWb+2t7wJm6cXILFnOROA=="
},
"node_modules/@types/ws": {
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz",
@ -5219,6 +5373,24 @@
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
},
"node_modules/bootstrap": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.2.3.tgz",
"integrity": "sha512-cEKPM+fwb3cT8NzQZYEu4HilJ3anCrWqh3CHAok1p9jXqMPsPTBhU25fBckEJHJ/p+tTxTFTsFQGM+gaHpi3QQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"@popperjs/core": "^2.11.6"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@ -5479,6 +5651,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA=="
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/clean-css": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz",
@ -6292,6 +6469,14 @@
"node": ">= 0.8"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"engines": {
"node": ">=6"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@ -6412,6 +6597,15 @@
"utila": "~0.4"
}
},
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dom-serializer": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
@ -8787,6 +8981,14 @@
"node": ">= 0.4"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/ipaddr.js": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz",
@ -13755,6 +13957,23 @@
"react-is": "^16.13.1"
}
},
"node_modules/prop-types-extra": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
"dependencies": {
"react-is": "^16.3.2",
"warning": "^4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0"
}
},
"node_modules/prop-types-extra/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -13935,6 +14154,35 @@
"node": ">=14"
}
},
"node_modules/react-bootstrap": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.7.2.tgz",
"integrity": "sha512-WDSln+mG4RLLFO01stkj2bEx/3MF4YihK9D/dWnHaSxOiQZLbhhlf95D2Jb20X3t2m7vMxRe888FVrfLJoGmmA==",
"dependencies": {
"@babel/runtime": "^7.17.2",
"@restart/hooks": "^0.4.6",
"@restart/ui": "^1.4.1",
"@types/react-transition-group": "^4.4.4",
"classnames": "^2.3.1",
"dom-helpers": "^5.2.1",
"invariant": "^2.2.4",
"prop-types": "^15.8.1",
"prop-types-extra": "^1.1.0",
"react-transition-group": "^4.4.2",
"uncontrollable": "^7.2.1",
"warning": "^4.0.3"
},
"peerDependencies": {
"@types/react": ">=16.14.8",
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -14074,6 +14322,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-lifecycles-compat": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@ -14154,6 +14407,21 @@
}
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -15892,6 +16160,20 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/uncontrollable": {
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@types/react": ">=16.9.11",
"invariant": "^2.2.4",
"react-lifecycles-compat": "^3.0.4"
},
"peerDependencies": {
"react": ">=15.0.0"
}
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@ -16100,6 +16382,14 @@
"makeerror": "1.0.12"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

@ -3,6 +3,7 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@react-spring/web": "^9.7.2",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@ -10,7 +11,10 @@
"@types/node": "^16.18.23",
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"bootstrap": "^5.2.3",
"csstype": "^3.1.2",
"react": "^18.2.0",
"react-bootstrap": "^2.7.2",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",

@ -1,25 +1,104 @@
import React from 'react';
import logo from './logo.svg';
import React, {ChangeEvent, useCallback, useMemo, useState} from 'react';
import './App.css';
import {Col, Container, Form, InputGroup, Row,} from "react-bootstrap";
import {Character, CharacterStatus, SPType} from "./CharacterStatus";
function App() {
const [maxHp, _setMaxHp] = useState(1)
const [hp, _setHp] = useState(1)
const setHp = useCallback(function (v: number) {
v = Math.floor(v)
if (Number.isNaN(v) || v < 0 || v > maxHp || v === hp) {
return
}
_setHp(v)
}, [hp, maxHp])
const setHpMax = useCallback(function (v: number) {
v = Math.floor(v)
if (Number.isNaN(v) || v < 0 || v > 9999 || v === maxHp) {
return
}
if (v < hp) {
setHp(v)
}
_setMaxHp(v)
}, [hp, maxHp, setHp])
const onHpChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setHp(parseInt(e.target.value)), [setHp])
const onMaxHpChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setHpMax(parseInt(e.target.value)), [setHpMax])
const [maxMp, _setMaxMp] = useState(6)
const [mp, _setMp] = useState(6)
const setMp = useCallback(function (v: number) {
v = Math.floor(v)
if (Number.isNaN(v) || v < 0 || v > maxMp || v === mp) {
return
}
_setMp(v)
}, [mp, maxMp])
const setMpMax = useCallback(function (v: number) {
v = Math.floor(v)
if (Number.isNaN(v) || v < 0 || v > 9999 || v === maxMp) {
return
}
if (v < mp) {
setMp(v)
}
_setMaxMp(v)
}, [mp, maxMp, setMp])
const onMpChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setMp(parseInt(e.target.value)), [setMp])
const onMaxMpChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setMpMax(parseInt(e.target.value)), [setMpMax])
const character = useMemo<Character>(() => ({
name: "Test",
level: 50,
hp: 40,
maxHp: 50,
mp: 40,
maxMp: 50,
ip: mp,
maxIp: maxMp,
sp: 1,
spType: SPType.UltimaPoints,
turnsLeft: hp,
turnsTotal: maxHp,
}), [hp, maxHp, mp, maxMp])
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
<Container fluid>
<Row>
<Col>
<InputGroup>
<InputGroup.Text>Max HP</InputGroup.Text>
<Form.Control type="numeric" max="9999" min="0" step="1" value={maxHp} onChange={onMaxHpChange} />
</InputGroup>
</Col>
<Col>
<InputGroup>
<InputGroup.Text>Current HP</InputGroup.Text>
<Form.Control type="numeric" max={maxHp} min="0" step="1" value={hp} onChange={onHpChange} />
</InputGroup>
</Col>
</Row>
<Row>
<Col>
<InputGroup>
<InputGroup.Text>Max MP</InputGroup.Text>
<Form.Control type="numeric" max="9999" min="0" step="1" value={maxMp} onChange={onMaxMpChange} />
</InputGroup>
</Col>
<Col>
<InputGroup>
<InputGroup.Text>Current MP</InputGroup.Text>
<Form.Control type="numeric" max={maxMp} min="0" step="1" value={mp} onChange={onMpChange} />
</InputGroup>
</Col>
</Row>
<Row>
<CharacterStatus character={character} />
</Row>
</Container>
);
}

@ -0,0 +1,181 @@
.characterStatus {
height: 150px;
width: 500px;
position: relative;
background-color: #454;
}
.characterHeader {
position: absolute;
left: 160px;
bottom: 60px;
z-index: 4;
}
.characterLevel {
display: inline;
color: white;
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.2);
text-shadow: 0 0 2px black;
margin-right: 0.25em;
}
.characterLevelLabel {
font-size: smaller;
font-variant: small-caps;
}
.characterName {
display: inline;
color: white;
font-family: sans-serif;
font-weight: bold;
font-size: 30px;
text-align: left;
-webkit-text-stroke: 1px rgba(0, 0, 0, 0.5);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.characterHpBar, .characterMpBar, .characterIpBar {
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
border: 2px solid black;
transform: skewX(-30deg) translateX(15px);
border-radius: 5px;
position: absolute;
width: calc(100% - 30px);
margin: 2px 2px 8px 5px;
bottom: 0;
}
.characterHpValue, .characterMpValue, .characterIpValue {
color: white;
font-family: sans-serif;
font-weight: bold;
font-style: italic;
font-size: 60px;
text-align: right;
-webkit-text-stroke: 1px black;
text-shadow: 2px 2px rgba(0, 0, 0, 0.5);
position: absolute;
bottom: 5px;
}
.characterHp {
position: absolute;
height: 60px;
left: 112px;
right: 0;
bottom: 28px;
overflow: visible;
z-index: 1;
}
.characterHpBar {
height: 25px;
}
.characterHpValue {
font-size: 60px;
right: 5px;
}
.characterMp, .characterIp {
position: absolute;
bottom: 0;
height: 40px;
}
.characterMp {
left: 95px;
right: 155px;
z-index: 3;
}
.characterIp {
width: 150px;
right: 20px;
z-index: 2;
}
.characterMpBar, .characterIpBar {
height: 20px;
}
.characterMpValue, .characterIpValue {
font-size: 40px;
right: 10px;
}
.characterMpBar {
height: 20px;
}
.characterMpValue {
font-size: 40px;
}
.characterPortrait {
position: absolute;
top: 10px;
bottom: 15px;
left: 50px;
width: 125px;
background: no-repeat center / cover;
z-index: 0;
}
.characterTurns {
position: absolute;
top: 5px;
left: 5px;
width: 40px;
height: 40px;
font-size: 40px;
font-weight: bold;
-webkit-text-stroke: 2px black;
text-shadow: 2px 2px 2px black ;
line-height: 40px;
text-align: center;
box-sizing: border-box;
border: 2px solid black;
border-radius: 9px 0;
color: white;
background-color: deepskyblue;
}
.characterTurnsDone {
background-color: darkblue;
}
.characterSp {
position: absolute;
color: white;
line-height: 40px;
-webkit-text-stroke: 2px black;
text-shadow: 2px 2px 2px black ;
font-size: 30px;
letter-spacing: -3px;
text-align: center;
font-weight: bold;
bottom: 5px;
left: 5px;
width: 40px;
height: 40px;
}
.characterSpFabula {
background: url("fabula-points.svg");
}
.characterSpUltima {
background: url("ultima-points.svg");
}
.characterSp {
opacity: 50%;
transition: opacity 0.3s ease-out;
}
.characterSp:hover {
opacity: 100%;
}

@ -0,0 +1,232 @@
import { animated } from "@react-spring/web";
import {ReactElement, useMemo} from "react";
import {
evaluateResourceBarStyles,
ResourceBarColors,
ResourceBarStyles
} from "./resource_bar";
import {isDefined} from "./type_check";
import {
SpringyValueInterpolatables,
useSpringyValue
} from "./SpringyValueHook";
import "./CharacterStatus.css";
export enum CharacterHealth {
Full = "Full",
Healthy = "Healthy",
Wounded = "Wounded",
Crisis = "Crisis",
Peril = "Peril",
KO = "KO",
}
export enum CharacterTurnState {
None = "None",
Ready = "Ready",
HighTurns = "HighTurns",
Done = "Done",
CantAct = "CantAct",
KO = "KO",
}
export enum SPType {
UltimaPoints = "Ultima",
FabulaPoints = "Fabula",
}
export interface StatusEffect {
name: string
iconUrl: string
}
export interface Character {
portraitUrl?: string
name?: string
level?: number
hp?: number
maxHp?: number
health?: CharacterHealth
mp?: number
maxMp?: number
ip?: number
maxIp?: number
sp?: number
spType?: SPType
turnsLeft?: number
turnsTotal?: number
canAct?: boolean
statuses?: StatusEffect[]
}
const hpBarStyle: SpringyValueInterpolatables<ResourceBarStyles> = {
foreground: 'linear-gradient(to bottom, rgb(255, 255, 255, 0.1) 0%, rgb(0, 0, 0, 0) 30% 50%, rgb(0, 0, 0, 0.1) 80%, rgb(0, 0, 0, 0.2) 95%, rgb(0, 0, 0, 0.3) 100%)',
barDirection: "to right",
barColors: ({flashValue}: {flashValue: number}): ResourceBarColors => {
return {
emptiedColor: `rgb(${Math.min(1, Math.max(flashValue, 0)) * 55 + 55}, 55, 55)`,
toEmptyColor: 'rgb(200, 0, 0)',
toFillColor: 'rgb(150, 250, 250)',
filledColor: 'rgb(0, 200, 0)',
}
},
}
const mpBarStyle: SpringyValueInterpolatables<ResourceBarStyles> = {
foreground: 'linear-gradient(to bottom, rgb(255, 255, 255, 0.1) 0%, rgb(0, 0, 0, 0) 30% 50%, rgb(0, 0, 0, 0.1) 80%, rgb(0, 0, 0, 0.2) 95%, rgb(0, 0, 0, 0.3) 100%)',
barColors: ({flashValue}: {flashValue: number}): ResourceBarColors => {
return {
emptiedColor: `rgb(${Math.min(1, Math.max(flashValue, 0)) * 55 + 55}, 55, 55)`,
toEmptyColor: 'rgb(250, 250, 60)',
toFillColor: 'rgb(150, 250, 250)',
filledColor: 'rgb(0, 150, 255)',
}
},
}
const ipBarStyle: SpringyValueInterpolatables<ResourceBarStyles> = {
foreground: 'linear-gradient(to bottom, rgb(255, 255, 255, 0.1) 0%, rgb(0, 0, 0, 0) 30% 50%, rgb(0, 0, 0, 0.1) 80%, rgb(0, 0, 0, 0.2) 95%, rgb(0, 0, 0, 0.3) 100%)',
barColors: ({flashValue}: {flashValue: number}): ResourceBarColors => {
return {
emptiedColor: `rgb(${Math.min(1, Math.max(flashValue, 0)) * 55 + 55}, 55, 55)`,
toEmptyColor: 'rgb(160, 55, 195)',
toFillColor: 'rgb(250, 250, 60)',
filledColor: 'rgb(255, 150, 55)',
}
},
}
export function CharacterStatus({character}: {character: Character}): ReactElement {
const {name, level, health} = character
const {hp, maxHp} = character
const {interpolate: hpInterpolate} = useSpringyValue({
current: hp,
max: maxHp,
flash: isDefined(maxHp) && isDefined(hp) && hp * 2 < maxHp && hp > 0,
})
const {hpText, hpBarStyleInterpolated} = useMemo(() => {
if (isDefined(hp) && isDefined(maxHp) && maxHp > 0) {
return {
hpText: hpInterpolate(({recentValue}) => `${Math.round(recentValue)}`),
hpBarStyleInterpolated: evaluateResourceBarStyles(hpBarStyle, hpInterpolate),
}
} else {
return {}
}
}, [hp, maxHp, hpInterpolate])
const {mp, maxMp} = character
const {interpolate: mpInterpolate} = useSpringyValue({
current: mp,
max: maxMp,
flash: false,
})
const {mpText, mpBarStyleInterpolated} = useMemo(() => {
if (isDefined(mp) && isDefined(maxMp) && maxMp > 0) {
return {
mpText: mpInterpolate(({recentValue}) => `${Math.round(recentValue)}`),
mpBarStyleInterpolated: evaluateResourceBarStyles(mpBarStyle, mpInterpolate),
}
} else {
return {}
}
}, [mp, maxMp, mpInterpolate])
const {ip, maxIp} = character
const {interpolate: ipInterpolate} = useSpringyValue({
current: ip,
max: maxIp,
flash: false,
})
const {ipText, ipBarStyleInterpolated} = useMemo(() => {
if (isDefined(ip) && isDefined(maxIp) && maxIp > 0) {
return {
ipText: ipInterpolate(({recentValue}) => `${Math.round(recentValue)}`),
ipBarStyleInterpolated: evaluateResourceBarStyles(ipBarStyle, ipInterpolate),
}
} else {
return {}
}
}, [ip, maxIp, ipInterpolate])
const {sp, spType} = character
const {interpolate: spInterpolate} = useSpringyValue({
current: sp,
flash: isDefined(spType) && isDefined(sp) && sp > 0,
})
const {spText} = useMemo(() => {
if (isDefined(sp) && isDefined(spType)) {
return {
spText: spInterpolate(({recentValue}) => recentValue.toFixed(0))
}
} else {
return {}
}
}, [sp, spType, spInterpolate])
const {turnsLeft, turnsTotal, canAct} = character
const {turnsState, turnsText} = useMemo(() => {
if (isDefined(turnsTotal) && hp === 0 && isDefined(maxHp) && maxHp > 0) {
return {
turnsState: CharacterTurnState.KO,
}
} else if (isDefined(turnsTotal) && (canAct === false || turnsTotal === 0)) {
return {
turnsState: CharacterTurnState.CantAct,
}
} else if (isDefined(turnsTotal) && turnsLeft === 0) {
return {
turnsState: CharacterTurnState.Done,
}
} else if (turnsTotal === 1 && turnsLeft === 1) {
return {
turnsState: CharacterTurnState.Ready,
}
} else if (isDefined(turnsTotal) && turnsTotal > 1 && isDefined(turnsLeft)) {
return {
turnsState: CharacterTurnState.HighTurns,
turnsText: `${turnsLeft}`
}
} else {
return {
turnsState: CharacterTurnState.None
}
}
}, [hp, maxHp, canAct, turnsLeft, turnsTotal])
return <div className="characterStatus">
<div className={"characterPortrait"} />
{isDefined(turnsState) &&
<div className={"characterTurns characterTurns" + turnsState}>{turnsText}</div>}
<div className={"characterHeader"}>
{isDefined(level) &&
<div className="characterLevel">
<span className="characterLevelLabel">Lv</span>
<span className="characterLevelValue">{level}</span>
</div>}
{isDefined(name) &&
<div className={"characterName characterName" + (health ?? "Unknown")}>{name}</div>}
</div>
{isDefined(hpText) &&
<div className={"characterHp"}>
<animated.div className={"characterHpBar"} style={hpBarStyleInterpolated} />
<animated.div className={"characterHpValue"}>{hpText}</animated.div>
</div>}
{isDefined(mpText) &&
<div className={"characterMp"}>
<animated.div className={"characterMpBar"} style={mpBarStyleInterpolated} />
<animated.div className={"characterMpValue"}>{mpText}</animated.div>
</div>}
{isDefined(ipText) &&
<div className={"characterIp"}>
<animated.div className={"characterIpBar"} style={ipBarStyleInterpolated} />
<animated.div className={"characterIpValue"}>{ipText}</animated.div>
</div>}
{isDefined(spText) &&
<animated.div className={"characterSp characterSp" + spType}>
<animated.span className={"characterSpValue characterSpValue" + spType}>
{spText}</animated.span>
</animated.div>}
</div>
}

@ -0,0 +1,131 @@
import {useCallback, useMemo, useState} from "react";
import {Interpolation, SpringConfig, SpringValue, to, useSpring, useTrail} from "@react-spring/web";
export interface UseSpringyValueProps {
current?: number
max?: number
flash?: boolean
springDelays?: readonly [number, number, number]
springConfigs?: readonly [SpringConfig, SpringConfig, SpringConfig]
flashConfig?: SpringConfig
}
export interface UseSpringyValueOutput {
springs: {v: SpringValue}[]
flashSpring: {v: SpringValue}
interpolate: SpringyValueInterpolate
}
export interface SpringyValues {
currentValue?: number // The true current value of the resource, after the recent delta.
displayedValue: number // The displayed current value of the resource, after the recent delta.
newValue: number // The displayed value of the resource as of ~2 seconds ago.
recentValue: number // The end of the delta that's being applied.
maxValue?: number // The maximum value of the resource.
previousValue?: number // The original value before the most recent delta.
flashValue: number // The position in the oscillation of the flashing resource.
flashing?: boolean // Whether the resource is actively flashing.
}
const literalSymbol: unique symbol = Symbol("SpringyValueLiteralSymbol")
export type SpringyValueLiteral<T> = {[literalSymbol]: T}
export function markSpringyValueLiteral<T>(value: T): SpringyValueLiteral<T> {
return {[literalSymbol]: value}
}
export function isSpringyValueLiteral<T>(value: SpringyValueInterpolatable<T>): value is SpringyValueLiteral<T> {
return typeof value === "object" && value !== null && Object.hasOwn(value, literalSymbol)
}
export type SpringyValueInterpolator<T> = (v: SpringyValues) => T
export type SpringyValueInterpolatable<T> = SpringyValueLiteral<T> | Exclude<T, Function> | SpringyValueInterpolator<T>
export type SpringyValueInterpolate = <T>(v: SpringyValueInterpolatable<T>) => SpringyValueInterpolated<T>
export type SpringyValueInterpolated<T> = T | Interpolation<T>
export type SpringyValueInterpolatables<TargetType extends object> =
{[Property in keyof TargetType]: SpringyValueInterpolatable<TargetType[Property]>}
export type SpringyValueInterpolateds<TargetType extends object> =
{[Property in keyof TargetType]: SpringyValueInterpolated<TargetType[Property]>}
export function evaluateSpringyValueInterpolator<T>(f: SpringyValueInterpolatable<T>, v: SpringyValues): T {
if (f instanceof Function) {
return f(v)
} else if (isSpringyValueLiteral(f)) {
return f[literalSymbol]
} else {
return f
}
}
export function interpolateSpringyValueInterpolatables<T extends object>(values: SpringyValueInterpolatables<T>, interpolator: SpringyValueInterpolate): SpringyValueInterpolateds<T> {
const result: {[key: string]: SpringyValueInterpolated<any>} = {}
for (const [key, value] of Object.entries(values) as [string, SpringyValueInterpolatable<any>][]) {
result[key] = interpolator(value)
}
return result as SpringyValueInterpolateds<T>
}
const DEFAULT_SPRING_DELAYS = [0, 500, 50] as const
const DEFAULT_SPRING_CONFIGS: readonly [SpringConfig, SpringConfig, SpringConfig] = [{tension: 1200, friction: 40, precision: 0.1, round: 0.001, clamp: true}, {tension: 200, friction: 20, precision: 0.1, round: 0.001}, {mass: 2, tension: 200, friction: 90, precision: 0.1, round: 0.001}]
const DEFAULT_FLASH_CONFIG: SpringConfig = {
tension: 170,
friction: 26,
clamp: true,
}
export function useSpringyValue({current, max, flash, springConfigs=DEFAULT_SPRING_CONFIGS, springDelays=DEFAULT_SPRING_DELAYS, flashConfig=DEFAULT_FLASH_CONFIG}: UseSpringyValueProps): UseSpringyValueOutput {
const [lastCurrent, setLastCurrent] = useState(current)
const [wasFlashing, setWasFlashing] = useState(flash)
const [springs, barApi] = useTrail(3, () => ({
v: current ?? 0
}), [])
const [flashSpring, flashApi] = useSpring({
from: {v: 0},
to: flash ? [{v: 1}, {v: 0}] : [{v: 0}],
loop: flash,
config: flashConfig,
}, [])
const interpolate = useCallback(function <T>(v: SpringyValueInterpolatable<T>): SpringyValueInterpolated<T> {
if (v instanceof Function) {
return to([...springs.map(s => s.v), flashSpring.v],
(displayedValue: number, newValue: number, recentValue: number, flashValue: number) => v({
currentValue: current,
displayedValue,
newValue,
recentValue,
flashValue,
maxValue: max,
previousValue: lastCurrent,
flashing: flash ?? false
}))
} else if (isSpringyValueLiteral(v)) {
return v[literalSymbol]
} else {
return v
}
}, [current, lastCurrent, max, flash, springs, flashSpring.v])
if (flash !== wasFlashing) {
if (flash && !wasFlashing) {
flashApi.start({to: [{v: 1}, {v: 0}], loop: true})
} else if (!flash && wasFlashing) {
flashApi.start({to: [{v: 0}], loop: false})
}
setWasFlashing(flash)
}
if (current !== lastCurrent) {
barApi.stop(true)
if ((lastCurrent ?? 0) < (current ?? 0)) {
barApi.set((idx, ctrl) => ({ v: Math.max(lastCurrent ?? 0, ctrl.get().v) }))
} else {
barApi.set((idx, ctrl) => ({ v: Math.min(lastCurrent ?? 0, ctrl.get().v) }))
}
barApi.start((i) => ({to: [{v: current ?? 0}], immediate: false, delay: springDelays[i], config: springConfigs[i]}))
setLastCurrent(current)
}
return useMemo(() => ({
springs,
flashSpring,
interpolate,
}), [springs, flashSpring, interpolate])
}

@ -0,0 +1,20 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%">
<feFlood flood-color="rgba(55, 55, 55, 1)" result="flood" />
<feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite" />
<feGaussianBlur in="composite" stdDeviation="15" result="blur" />
<feOffset dx="0" dy="0" result="offset" />
<feComposite in="SourceGraphic" in2="offset" />
</filter>
<radialGradient id="lorc-star-swirl-gradient-1">
<stop offset="0%" stop-color="#f8e71c" stop-opacity="1" />
<stop offset="100%" stop-color="#f8e71c" stop-opacity="0.28" />
</radialGradient>
</defs>
<g class="" style="" transform="translate(0,0)">
<path d="M436.406 29.625l-18.094 42.22-48.562 5.905 42.156 25.656 1.375 13.47C367.938 90.74 302.435 75.36 214.78 82.31l-20.186-3.343-24.125-38.407.5 39.78-49.22 16.438 55.063 4.564 7.843 33.78 17.094-37.78 17.906-2.75c203.993 22.03 277.475 204.75 77.875 207.625l5.22-37.595 36.75-43.72-51.344-24.968-30.22-48.468-39.623 41.124-4.125 1.03C-8.4 163.078-31.708 304.485 98.844 376.125l-11.938 12.688L39.844 374.5l33.03 39.406-15.124 42.53 36.375-31.155 47.03 18.095-30.374-43.875 4.69-15.03c62.43 28.648 153.852 42.16 270.5 20.717-241.042 33.38-364.142-137.94-219.283-195.687l23.032 43.25-4 56.97 56.218-9.97 19.25 7.813c218.255 102.608 297.46-83.917 171.843-177.75l14.376-22.22 46.47-16.5-41.907-14.812-15.564-46.655zM34.53 79.03l4.845 26.095-19.47 22 27.22-3.25 17.563 23.344.687-29.47 24.78-17.906-33.218-1.72-22.406-19.093zm358.564 298.5l14.25 51.658-31.375 41.062 49.592-12.688 33.688 34.282-2.53-51.406 35.217-30.375-51.593 1.562-47.25-34.094z"
fill="url(#lorc-star-swirl-gradient-1)" filter="url(#shadow-1)" stroke="#f8ebaa" stroke-opacity="1"
stroke-width="8" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

@ -0,0 +1,81 @@
import CSS from "csstype";
import {
evaluateSpringyValueInterpolator, interpolateSpringyValueInterpolatables,
SpringyValueInterpolatables,
SpringyValueInterpolate,
SpringyValueInterpolateds,
SpringyValues
} from "./SpringyValueHook";
import {isDefined} from "./type_check";
export interface ResourceBarColors {
filledColor: CSS.Property.Color
toFillColor: CSS.Property.Color
toEmptyColor: CSS.Property.Color
emptiedColor: CSS.Property.Color
}
export function resourceGradient(
{filledColor, toFillColor, toEmptyColor, emptiedColor}: ResourceBarColors,
{displayedValue, recentValue, newValue, maxValue}: SpringyValues,
direction: string): string {
const effectiveMax = isDefined(maxValue) && maxValue !== 0 ? maxValue : Number.POSITIVE_INFINITY
const currentStop = Math.min(Math.max(100 * displayedValue / effectiveMax, 0), 100)
const currentSoftEdgeLeft = Math.min(Math.max(currentStop - 0.2, 0), 100)
const currentSoftEdgeRight = Math.min(Math.max(currentStop + 0.2, 0), 100)
const endFillingStop = Math.min(Math.max(100 * recentValue / effectiveMax, 0), currentStop)
const midFillingStop = Math.min(Math.max(100 * newValue / effectiveMax, endFillingStop), currentStop)
const endFadingStop = Math.min(Math.max(100 * recentValue / effectiveMax, currentStop), 100)
const midFadingStop =
Math.min(Math.max(100 * newValue / effectiveMax, currentStop), endFadingStop)
const layers = []
if (currentStop === 100) {
layers.unshift(filledColor)
} else {
layers.unshift(emptiedColor)
if (currentStop > 0) {
layers.unshift(`linear-gradient(${direction}, ${filledColor} 0% ${currentSoftEdgeLeft.toFixed(4)}%, transparent ${currentSoftEdgeRight.toFixed(4)}% 100%)`)
}
}
if (endFillingStop !== currentStop || midFillingStop !== currentStop) {
layers.unshift(`linear-gradient(${direction}, transparent 0% ${endFillingStop.toFixed(4)}%, ${toFillColor} ${midFillingStop.toFixed(4)}% ${currentSoftEdgeLeft.toFixed(4)}%, transparent ${currentSoftEdgeRight.toFixed(4)}% 100%)`)
}
if (endFadingStop !== currentStop || midFadingStop !== currentStop) {
layers.unshift(`linear-gradient(${direction}, transparent 0% ${currentSoftEdgeLeft.toFixed(4)}%, ${toEmptyColor} ${currentSoftEdgeRight.toFixed(4)}% ${midFadingStop.toFixed(4)}%, transparent ${endFadingStop.toFixed(4)}% 100%)`)
}
return layers.join(", ")
}
export interface ResourceBarStyles extends CSS.Properties {
foreground?: CSS.Properties["background"]
barDirection?: string
barColors?: ResourceBarColors
}
export const DEFAULT_BAR_COLORS: ResourceBarColors = {
emptiedColor: 'black',
filledColor: 'limegreen',
toEmptyColor: 'red',
toFillColor: 'cyan',
} as const
export const DEFAULT_BAR_DIRECTION: string = "to right"
export function evaluateResourceBarStyles(barStyle: SpringyValueInterpolatables<ResourceBarStyles>, interpolate: SpringyValueInterpolate) {
const intermediate: SpringyValueInterpolatables<ResourceBarStyles> = Object.assign({}, barStyle)
delete intermediate.background
delete intermediate.foreground
delete intermediate.barColors
delete intermediate.barDirection
intermediate.background = (v: SpringyValues) => [
evaluateSpringyValueInterpolator(barStyle["foreground"], v) as string | undefined,
resourceGradient(
evaluateSpringyValueInterpolator(barStyle["barColors"], v) ?? DEFAULT_BAR_COLORS, v,
evaluateSpringyValueInterpolator(barStyle["barDirection"], v) ?? DEFAULT_BAR_DIRECTION),
evaluateSpringyValueInterpolator(barStyle["background"], v) as string | undefined
].filter((s) => !!s).join(', ')
return interpolateSpringyValueInterpolatables(intermediate as SpringyValueInterpolateds<CSS.Properties>, interpolate)
}

@ -0,0 +1,3 @@
export function isDefined<T>(i: T | undefined): i is T {
return typeof i !== "undefined"
}

@ -0,0 +1,20 @@
<svg style="height: 512px; width: 512px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<filter id="shadow-1" height="300%" width="300%" x="-100%" y="-100%">
<feFlood flood-color="rgba(0, 0, 0, 1)" result="flood" />
<feComposite in="flood" in2="SourceGraphic" operator="atop" result="composite" />
<feGaussianBlur in="composite" stdDeviation="15" result="blur" />
<feOffset dx="0" dy="0" result="offset" />
<feComposite in="SourceGraphic" in2="offset" />
</filter>
<radialGradient id="lorc-evil-moon-gradient-1">
<stop offset="0%" stop-color="#d0021b" stop-opacity="1" />
<stop offset="100%" stop-color="#f81c1c" stop-opacity="0.73" />
</radialGradient>
</defs>
<g class="" style="" transform="translate(0,0)">
<path d="M255.938 19.938C124.514 19.938 17.78 126.67 17.78 258.094c0 131.422 106.735 238.156 238.157 238.156 131.423 0 238.157-106.734 238.157-238.156 0-131.422-106.734-238.156-238.156-238.156zm0 18.687c121.322 0 219.468 98.147 219.468 219.47 0 26.08-4.548 51.085-12.875 74.28-20.99 8.188-43.686 12.75-67.624 12.75-22.242 0-43.584-3.855-63.406-10.938l12.094 44.594-71.75-12.467 18.844 51.562c-57.855 7.1-108.19-15.432-130.47-49.28l81.313 1.78-23.56-48.438 72.436-8.062.688.063c-16.83-11.135-31.777-24.876-44.22-40.688 30.674-18.014 66.44-28.814 102.782-22.844-43.234-49.45-95.713-64.09-139.437-57.437-2.188-11.438-3.345-23.236-3.345-35.314 0-45.286 16.2-86.562 42.938-118.937 2.04-.057 4.072-.095 6.125-.095zM139.188 180.78c.638-.006 1.268-.013 1.906 0 19.467.437 38.24 10.748 48.687 28.845 15.923 27.577 6.48 62.83-21.093 78.75-27.572 15.92-62.828 6.483-78.75-21.094-15.92-27.576-6.478-62.86 21.094-78.78 8.887-5.13 18.58-7.615 28.157-7.72zm-.063 21.064c-20.26 0-36.906 16.643-36.906 36.906 0 20.263 16.644 36.875 36.905 36.875 20.26 0 36.875-16.612 36.875-36.875s-16.614-36.906-36.875-36.906zm0 18.687c10.16 0 18.188 8.058 18.188 18.22 0 10.162-8.028 18.188-18.188 18.188-10.16 0-18.22-8.026-18.22-18.188 0-10.162 8.06-18.22 18.22-18.22z"
fill="url(#lorc-evil-moon-gradient-1)" stroke="#690303" stroke-opacity="1" stroke-width="8"
filter="url(#shadow-1)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Loading…
Cancel
Save