├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── assets ├── armor.png ├── attack-minion-premium.png ├── attack-minion.png ├── attack-weapon-premium.png ├── attack-weapon.png ├── base-hero-premium.png ├── base-minion-premium.png ├── base-spell-premium.png ├── base-weapon-premium.png ├── cost-health.png ├── cost-mana.png ├── durability-premium.png ├── durability.png ├── elite-hero-premium.png ├── elite-hero.png ├── elite-minion-premium.png ├── elite-minion.png ├── elite-spell-premium.png ├── elite-spell.png ├── elite-weapon-premium.png ├── elite-weapon.png ├── frame-hero-druid.png ├── frame-hero-hunter.png ├── frame-hero-mage.png ├── frame-hero-neutral.png ├── frame-hero-paladin.png ├── frame-hero-premium-deathknight.png ├── frame-hero-premium-druid.png ├── frame-hero-premium-hunter.png ├── frame-hero-premium-mage.png ├── frame-hero-premium-neutral.png ├── frame-hero-premium-paladin.png ├── frame-hero-premium-priest.png ├── frame-hero-premium-rogue.png ├── frame-hero-premium-shaman.png ├── frame-hero-premium-warlock.png ├── frame-hero-premium-warrior.png ├── frame-hero-priest.png ├── frame-hero-rogue.png ├── frame-hero-shaman.png ├── frame-hero-warlock.png ├── frame-hero-warrior.png ├── frame-minion-deathknight.png ├── frame-minion-druid.png ├── frame-minion-hunter.png ├── frame-minion-mage.png ├── frame-minion-neutral.png ├── frame-minion-paladin.png ├── frame-minion-premium-deathknight.png ├── frame-minion-premium-druid.png ├── frame-minion-premium-hunter.png ├── frame-minion-premium-mage.png ├── frame-minion-premium-neutral.png ├── frame-minion-premium-paladin.png ├── frame-minion-premium-priest.png ├── frame-minion-premium-rogue.png ├── frame-minion-premium-shaman.png ├── frame-minion-premium-warlock.png ├── frame-minion-premium-warrior.png ├── frame-minion-priest.png ├── frame-minion-rogue.png ├── frame-minion-shaman.png ├── frame-minion-warlock.png ├── frame-minion-warrior.png ├── frame-spell-deathknight.png ├── frame-spell-druid.png ├── frame-spell-hunter.png ├── frame-spell-mage.png ├── frame-spell-neutral.png ├── frame-spell-paladin.png ├── frame-spell-premium-deathknight.png ├── frame-spell-premium-druid.png ├── frame-spell-premium-hunter.png ├── frame-spell-premium-mage.png ├── frame-spell-premium-neutral.png ├── frame-spell-premium-paladin.png ├── frame-spell-premium-priest.png ├── frame-spell-premium-rogue.png ├── frame-spell-premium-shaman.png ├── frame-spell-premium-warlock.png ├── frame-spell-premium-warrior.png ├── frame-spell-priest.png ├── frame-spell-rogue.png ├── frame-spell-shaman.png ├── frame-spell-warlock.png ├── frame-spell-warrior.png ├── frame-weapon-deathknight.png ├── frame-weapon-druid.png ├── frame-weapon-hunter.png ├── frame-weapon-mage.png ├── frame-weapon-neutral.png ├── frame-weapon-paladin.png ├── frame-weapon-premium-deathknight.png ├── frame-weapon-premium-druid.png ├── frame-weapon-premium-hunter.png ├── frame-weapon-premium-mage.png ├── frame-weapon-premium-neutral.png ├── frame-weapon-premium-paladin.png ├── frame-weapon-premium-priest.png ├── frame-weapon-premium-rogue.png ├── frame-weapon-premium-shaman.png ├── frame-weapon-premium-warlock.png ├── frame-weapon-premium-warrior.png ├── frame-weapon-priest.png ├── frame-weapon-rogue.png ├── frame-weapon-shaman.png ├── frame-weapon-warlock.png ├── frame-weapon-warrior.png ├── health-premium.png ├── health.png ├── hero-power-opponent.png ├── hero-power-player.png ├── hero-power-premium-opponent.png ├── hero-power-premium-player.png ├── mPreload.jpg ├── multi-grimy_goons.png ├── multi-jade_lotus.png ├── multi-kabal.png ├── name-banner-hero-premium.png ├── name-banner-hero.png ├── name-banner-minion-premium.png ├── name-banner-minion.png ├── name-banner-spell-premium.png ├── name-banner-spell.png ├── name-banner-weapon-premium.png ├── name-banner-weapon.png ├── race-banner-premium.png ├── race-banner.png ├── rarity-common.png ├── rarity-epic.png ├── rarity-legendary.png ├── rarity-minion-common.png ├── rarity-minion-epic.png ├── rarity-minion-legendary.png ├── rarity-minion-premium-common.png ├── rarity-minion-premium-epic.png ├── rarity-minion-premium-legendary.png ├── rarity-minion-premium-rare.png ├── rarity-minion-rare.png ├── rarity-rare.png ├── rarity-spell-common.png ├── rarity-spell-epic.png ├── rarity-spell-legendary.png ├── rarity-spell-premium-common.png ├── rarity-spell-premium-epic.png ├── rarity-spell-premium-legendary.png ├── rarity-spell-premium-rare.png ├── rarity-spell-rare.png ├── rarity-weapon-common.png ├── rarity-weapon-epic.png ├── rarity-weapon-legendary.png ├── rarity-weapon-rare.png ├── sPreload.jpg ├── set-boomsday.png ├── set-brm.png ├── set-dalaran.png ├── set-expert1.png ├── set-gangs.png ├── set-gilneas.png ├── set-gvg.png ├── set-hof.png ├── set-icecrown.png ├── set-kara.png ├── set-loe.png ├── set-lootapalooza.png ├── set-naxx.png ├── set-og.png ├── set-tgt.png ├── set-troll.png ├── set-ungoro.png ├── silence-x.png └── wPreload.jpg ├── package.json ├── rollup.config.js ├── scripts └── sunwell-card-renderer.js ├── src ├── Card.ts ├── CardDef.ts ├── Components │ ├── AttackGem.ts │ ├── BodyText.ts │ ├── CardArt.ts │ ├── CardFrame.ts │ ├── Component.ts │ ├── CostGem.ts │ ├── EliteDragon.ts │ ├── Gem.ts │ ├── HealthGem.ts │ ├── MultiClassBanner.ts │ ├── NameBanner.ts │ ├── RaceBanner.ts │ ├── RarityGem.ts │ └── Watermark.ts ├── Enums.ts ├── HeroCard.ts ├── HeroCardPremium.ts ├── HeroPowerCard.ts ├── HeroPowerCardPremium.ts ├── MinionCard.ts ├── MinionCardPremium.ts ├── SpellCard.ts ├── SpellCardPremium.ts ├── Sunwell.ts ├── WeaponCard.ts ├── WeaponCardPremium.ts ├── helpers.ts ├── interfaces.ts └── platforms │ ├── IPlatform.ts │ ├── NodePlatform.ts │ └── WebPlatform.ts ├── tests └── test.html ├── tsconfig.json ├── tslint.json ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://editorconfig.org 2 | # https://hearthsim.info/styleguide/ 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 4 10 | indent_style = tab 11 | quote_type = double 12 | insert_final_newline = true 13 | tab_width = 4 14 | trim_trailing_whitespace = true 15 | 16 | [*.{js,ts,tsx}] 17 | spaces_around_brackets = none 18 | spaces_around_operators = true 19 | 20 | [*.{css,js,ts,tsx}] 21 | indent_brace_style = BSD KNF 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | 4 | /dist/ 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | /tests/ 3 | /assets/ 4 | yarn.lock 5 | .travis.yml 6 | /webpack.config.js 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://travis-ci.org/HearthSim/Sunwell 2 | language: node_js 3 | node_js: 4 | - 8 5 | - "node" 6 | 7 | cache: yarn 8 | 9 | addons: 10 | apt: 11 | sources: 12 | - ubuntu-toolchain-r-test 13 | packages: 14 | - g++-6 15 | - libgif-dev 16 | - libcairo2-dev 17 | - libjpeg8-dev 18 | - libpango1.0-dev 19 | 20 | env: 21 | - CXX=g++-6 22 | 23 | jobs: 24 | include: 25 | - stage: deploy 26 | if: type = push 27 | node_js: "node" 28 | install: 29 | - yarn --pure-lockfile 30 | 31 | script: 32 | - yarn run lint 33 | - yarn run build 34 | - NODE_ENV=production yarn run build 35 | - cp -r assets/ dist/ 36 | 37 | deploy: 38 | provider: npm 39 | email: $NPM_EMAIL 40 | api_key: $NPM_AUTH_TOKEN 41 | skip_cleanup: true 42 | on: 43 | tags: true 44 | repo: HearthSim/Sunwell 45 | 46 | script: 47 | - yarn run build 48 | - NODE_ENV=production yarn run build 49 | - yarn run qa 50 | 51 | notifications: 52 | irc: 53 | channels: 54 | - "chat.freenode.net#hearthsim-commits" 55 | use_notice: true 56 | skip_join: true 57 | on_failure: always 58 | on_success: change 59 | template: 60 | - "(%{branch} @ %{commit} : %{author}): %{message} %{build_url}" 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 HearthSim 2 | Copyright (c) 2016 Christian Engel 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project is no longer maintained! Please see the [forks page](https://github.com/HearthSim/Sunwell/network) for a list of alternative repositores.** 2 | 3 | # Sunwell 4 | 5 | A high quality Hearthstone card renderer in TypeScript. 6 | 7 | * Use it as a Javascript library in the browser to render to a `` element. 8 | * Or in NodeJS using [node-canvas](https://github.com/Automattic/node-canvas) 9 | 10 | 11 | ## Download 12 | 13 | The latest, minified version of Sunwell is available here: 14 | 15 | https://sunwell.hearthsim.net/branches/master/sunwell.cdn.min.js 16 | 17 | 18 | ## Requirements 19 | 20 | The following dependencies are required to build Sunwell: 21 | 22 | - [Yarn](https://github.com/yarnpkg/yarn) 23 | - [Webpack](https://github.com/webpack/webpack) 24 | 25 | Run `yarn install` to install the dependencies. Then, run `webpack` to build 26 | the project into `dist/sunwell.js`. 27 | 28 | If you run `NODE_ENV=production webpack`, it will instead build a minified 29 | version into `dist/sunwell.min.js`. 30 | 31 | The assets in the `assets/` folder can be copied as-is. 32 | 33 | Sunwell currently has no runtime dependencies outside of Webpack. 34 | 35 | 36 | ### Fonts 37 | 38 | To create faithful renders, the Belwe and Franklin Gothic proprietary fonts are required. 39 | Make sure to obtain a license if you wish to distribute cards rendered with them. 40 | 41 | Sunwell should work with any fallback font you give it. 42 | 43 | 44 | ## Usage 45 | 46 | Instanciate a new `Sunwell` object as such: 47 | 48 | ```js 49 | const Sunwell = require("Sunwell").Sunwell; 50 | let sunwell = new Sunwell(options); 51 | ``` 52 | 53 | The `options` object is defined further down. 54 | 55 | To render a card, you can use `Sunwell.createCard(card)`. 56 | Sunwell aims to be compatible with [HearthstoneJSON](https://hearthstonejson.com). 57 | You can pass a `card` object from the HearthstoneJSON API as-is and get a usable card in return. 58 | 59 | 60 | ```js 61 | var card = sunwell.createCard(card, width, target, callback); 62 | ``` 63 | 64 | `width` is the size of the render (height is determined automatically). 65 | The `target` argument should be a Canvas or Image object the render will be applied to. 66 | 67 | Internally, Sunwell renders to a Canvas already. If you target an image, the conversion 68 | to PNG and compression will result in performance loss with frequent updates. 69 | Rendering to a Canvas is more direct, however will likely result in performance degradation 70 | with large amounts of cards on screen. Pick your poison. 71 | 72 | The optional `callback` argument is a function called when the rendering finishes. 73 | 74 | 75 | ### Sunwell options 76 | 77 | The following options can be forwarded to the Sunwell instance: 78 | 79 | - `debug` (boolean: `false`): If `true`, extra output will be logged. 80 | - `titleFont` (string: `"Belwe"`): The font to use for the card's name. 81 | - `bodyFont` (string: `"Franklin Gothic"`): The font to use for the card's body. 82 | - `assetFolder` (string: `"/assets/"`): The path to the assets folder. 83 | - `cacheSkeleton` (boolean: `false`): Whether to cache the card's frame render (slow). 84 | - `drawTimeout` (number: `5000`): The maximum amount of milliseconds Sunwell will spend 85 | rendering any single card before giving up. 86 | - `maxActiveRenders` (number: `12`): How many concurrent renders Sunwell will perform. 87 | - `preloadedAssets` (Array): A list of assets to always preload. 88 | 89 | 90 | ### Card properties 91 | 92 | The following card properties are supported: 93 | 94 | - `name` (string): The card's name 95 | - `text` (string): The card's body text 96 | - `collectionText` (string): The card's body text. Has precedence over `text`. 97 | - `raceText` (string): The card's race as a string. Has precedence over `race`. 98 | - `cost` (number): The card's cost. 99 | - `attack` (number): The card's attack value (has no effect for spells). 100 | - `health` (number): The card's health value (has no effect for spells). 101 | - `costsHealth` (boolean): Set to true to render the card's cost as health. 102 | - `hideStats` (boolean): Set to true to hide the attack/health textures and the cost value. 103 | - `silenced` (boolean): Set to true to show the card as silenced (card text crossed out). 104 | - `language` (string): The language of the card. Only used to determine the race text for enums. 105 | 106 | 107 | Enums are standard Hearthstone enums. They can be passed as an int (preferred) or as their 108 | string variant: 109 | 110 | - `type` (enum CardType): The card's type (only MINION, SPELL and WEAPON are supported). 111 | - `cardClass` (enum CardClass): The card's class. This determines the card frame to use. 112 | - `set` (enum CardSet): Determines the body text background watermark. 113 | - `rarity` (enum Rarity): Determines the card's rarity gem. Note that COMMON cards from the 114 | CORE set will not show a rarity gem, despite not being FREE. 115 | - `multiClassGroup` (enum MultiClassGroup): Determines the card's multiclass banner. 116 | - `race` (enum Race): The card's race. 117 | 118 | Finally, a `texture` property should be passed as well, for the card art. The texture may be 119 | a string, in which case it's treated as a URL, or it may be an Image. If null, an empty grey 120 | texture will be created in its place. 121 | 122 | NOTE: Some properties only affect certain card types unless explicitly set on the card 123 | itself. For example, a spell or weapon will not render a race text even if given one. 124 | 125 | 126 | ## Community 127 | 128 | Sunwell is a [HearthSim](https://hearthsim.info) project. All development 129 | happens on our IRC channel `#hearthsim` on [Freenode](https://freenode.net). 130 | 131 | 132 | # License 133 | 134 | This project is licensed under the MIT license. The full license text is 135 | available in the LICENSE file. 136 | 137 | The assets directory includes files that are copyright © Blizzard Entertainment. 138 | -------------------------------------------------------------------------------- /assets/armor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/armor.png -------------------------------------------------------------------------------- /assets/attack-minion-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/attack-minion-premium.png -------------------------------------------------------------------------------- /assets/attack-minion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/attack-minion.png -------------------------------------------------------------------------------- /assets/attack-weapon-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/attack-weapon-premium.png -------------------------------------------------------------------------------- /assets/attack-weapon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/attack-weapon.png -------------------------------------------------------------------------------- /assets/base-hero-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/base-hero-premium.png -------------------------------------------------------------------------------- /assets/base-minion-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/base-minion-premium.png -------------------------------------------------------------------------------- /assets/base-spell-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/base-spell-premium.png -------------------------------------------------------------------------------- /assets/base-weapon-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/base-weapon-premium.png -------------------------------------------------------------------------------- /assets/cost-health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/cost-health.png -------------------------------------------------------------------------------- /assets/cost-mana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/cost-mana.png -------------------------------------------------------------------------------- /assets/durability-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/durability-premium.png -------------------------------------------------------------------------------- /assets/durability.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/durability.png -------------------------------------------------------------------------------- /assets/elite-hero-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-hero-premium.png -------------------------------------------------------------------------------- /assets/elite-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-hero.png -------------------------------------------------------------------------------- /assets/elite-minion-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-minion-premium.png -------------------------------------------------------------------------------- /assets/elite-minion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-minion.png -------------------------------------------------------------------------------- /assets/elite-spell-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-spell-premium.png -------------------------------------------------------------------------------- /assets/elite-spell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-spell.png -------------------------------------------------------------------------------- /assets/elite-weapon-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-weapon-premium.png -------------------------------------------------------------------------------- /assets/elite-weapon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/elite-weapon.png -------------------------------------------------------------------------------- /assets/frame-hero-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-druid.png -------------------------------------------------------------------------------- /assets/frame-hero-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-hunter.png -------------------------------------------------------------------------------- /assets/frame-hero-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-mage.png -------------------------------------------------------------------------------- /assets/frame-hero-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-neutral.png -------------------------------------------------------------------------------- /assets/frame-hero-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-paladin.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-deathknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-deathknight.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-druid.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-hunter.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-mage.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-neutral.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-paladin.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-priest.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-rogue.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-shaman.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-warlock.png -------------------------------------------------------------------------------- /assets/frame-hero-premium-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-premium-warrior.png -------------------------------------------------------------------------------- /assets/frame-hero-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-priest.png -------------------------------------------------------------------------------- /assets/frame-hero-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-rogue.png -------------------------------------------------------------------------------- /assets/frame-hero-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-shaman.png -------------------------------------------------------------------------------- /assets/frame-hero-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-warlock.png -------------------------------------------------------------------------------- /assets/frame-hero-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-hero-warrior.png -------------------------------------------------------------------------------- /assets/frame-minion-deathknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-deathknight.png -------------------------------------------------------------------------------- /assets/frame-minion-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-druid.png -------------------------------------------------------------------------------- /assets/frame-minion-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-hunter.png -------------------------------------------------------------------------------- /assets/frame-minion-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-mage.png -------------------------------------------------------------------------------- /assets/frame-minion-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-neutral.png -------------------------------------------------------------------------------- /assets/frame-minion-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-paladin.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-deathknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-deathknight.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-druid.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-hunter.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-mage.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-neutral.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-paladin.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-priest.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-rogue.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-shaman.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-warlock.png -------------------------------------------------------------------------------- /assets/frame-minion-premium-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-premium-warrior.png -------------------------------------------------------------------------------- /assets/frame-minion-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-priest.png -------------------------------------------------------------------------------- /assets/frame-minion-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-rogue.png -------------------------------------------------------------------------------- /assets/frame-minion-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-shaman.png -------------------------------------------------------------------------------- /assets/frame-minion-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-warlock.png -------------------------------------------------------------------------------- /assets/frame-minion-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-minion-warrior.png -------------------------------------------------------------------------------- /assets/frame-spell-deathknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-deathknight.png -------------------------------------------------------------------------------- /assets/frame-spell-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-druid.png -------------------------------------------------------------------------------- /assets/frame-spell-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-hunter.png -------------------------------------------------------------------------------- /assets/frame-spell-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-mage.png -------------------------------------------------------------------------------- /assets/frame-spell-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-neutral.png -------------------------------------------------------------------------------- /assets/frame-spell-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-paladin.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-deathknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-deathknight.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-druid.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-hunter.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-mage.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-neutral.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-paladin.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-priest.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-rogue.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-shaman.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-warlock.png -------------------------------------------------------------------------------- /assets/frame-spell-premium-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-premium-warrior.png -------------------------------------------------------------------------------- /assets/frame-spell-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-priest.png -------------------------------------------------------------------------------- /assets/frame-spell-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-rogue.png -------------------------------------------------------------------------------- /assets/frame-spell-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-shaman.png -------------------------------------------------------------------------------- /assets/frame-spell-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-warlock.png -------------------------------------------------------------------------------- /assets/frame-spell-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-spell-warrior.png -------------------------------------------------------------------------------- /assets/frame-weapon-deathknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-deathknight.png -------------------------------------------------------------------------------- /assets/frame-weapon-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-druid.png -------------------------------------------------------------------------------- /assets/frame-weapon-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-hunter.png -------------------------------------------------------------------------------- /assets/frame-weapon-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-mage.png -------------------------------------------------------------------------------- /assets/frame-weapon-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-neutral.png -------------------------------------------------------------------------------- /assets/frame-weapon-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-paladin.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-deathknight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-deathknight.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-druid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-druid.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-hunter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-hunter.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-mage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-mage.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-neutral.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-neutral.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-paladin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-paladin.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-priest.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-rogue.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-shaman.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-warlock.png -------------------------------------------------------------------------------- /assets/frame-weapon-premium-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-premium-warrior.png -------------------------------------------------------------------------------- /assets/frame-weapon-priest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-priest.png -------------------------------------------------------------------------------- /assets/frame-weapon-rogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-rogue.png -------------------------------------------------------------------------------- /assets/frame-weapon-shaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-shaman.png -------------------------------------------------------------------------------- /assets/frame-weapon-warlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-warlock.png -------------------------------------------------------------------------------- /assets/frame-weapon-warrior.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/frame-weapon-warrior.png -------------------------------------------------------------------------------- /assets/health-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/health-premium.png -------------------------------------------------------------------------------- /assets/health.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/health.png -------------------------------------------------------------------------------- /assets/hero-power-opponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/hero-power-opponent.png -------------------------------------------------------------------------------- /assets/hero-power-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/hero-power-player.png -------------------------------------------------------------------------------- /assets/hero-power-premium-opponent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/hero-power-premium-opponent.png -------------------------------------------------------------------------------- /assets/hero-power-premium-player.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/hero-power-premium-player.png -------------------------------------------------------------------------------- /assets/mPreload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/mPreload.jpg -------------------------------------------------------------------------------- /assets/multi-grimy_goons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/multi-grimy_goons.png -------------------------------------------------------------------------------- /assets/multi-jade_lotus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/multi-jade_lotus.png -------------------------------------------------------------------------------- /assets/multi-kabal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/multi-kabal.png -------------------------------------------------------------------------------- /assets/name-banner-hero-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-hero-premium.png -------------------------------------------------------------------------------- /assets/name-banner-hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-hero.png -------------------------------------------------------------------------------- /assets/name-banner-minion-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-minion-premium.png -------------------------------------------------------------------------------- /assets/name-banner-minion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-minion.png -------------------------------------------------------------------------------- /assets/name-banner-spell-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-spell-premium.png -------------------------------------------------------------------------------- /assets/name-banner-spell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-spell.png -------------------------------------------------------------------------------- /assets/name-banner-weapon-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-weapon-premium.png -------------------------------------------------------------------------------- /assets/name-banner-weapon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/name-banner-weapon.png -------------------------------------------------------------------------------- /assets/race-banner-premium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/race-banner-premium.png -------------------------------------------------------------------------------- /assets/race-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/race-banner.png -------------------------------------------------------------------------------- /assets/rarity-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-common.png -------------------------------------------------------------------------------- /assets/rarity-epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-epic.png -------------------------------------------------------------------------------- /assets/rarity-legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-legendary.png -------------------------------------------------------------------------------- /assets/rarity-minion-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-common.png -------------------------------------------------------------------------------- /assets/rarity-minion-epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-epic.png -------------------------------------------------------------------------------- /assets/rarity-minion-legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-legendary.png -------------------------------------------------------------------------------- /assets/rarity-minion-premium-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-premium-common.png -------------------------------------------------------------------------------- /assets/rarity-minion-premium-epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-premium-epic.png -------------------------------------------------------------------------------- /assets/rarity-minion-premium-legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-premium-legendary.png -------------------------------------------------------------------------------- /assets/rarity-minion-premium-rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-premium-rare.png -------------------------------------------------------------------------------- /assets/rarity-minion-rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-minion-rare.png -------------------------------------------------------------------------------- /assets/rarity-rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-rare.png -------------------------------------------------------------------------------- /assets/rarity-spell-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-common.png -------------------------------------------------------------------------------- /assets/rarity-spell-epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-epic.png -------------------------------------------------------------------------------- /assets/rarity-spell-legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-legendary.png -------------------------------------------------------------------------------- /assets/rarity-spell-premium-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-premium-common.png -------------------------------------------------------------------------------- /assets/rarity-spell-premium-epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-premium-epic.png -------------------------------------------------------------------------------- /assets/rarity-spell-premium-legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-premium-legendary.png -------------------------------------------------------------------------------- /assets/rarity-spell-premium-rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-premium-rare.png -------------------------------------------------------------------------------- /assets/rarity-spell-rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-spell-rare.png -------------------------------------------------------------------------------- /assets/rarity-weapon-common.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-weapon-common.png -------------------------------------------------------------------------------- /assets/rarity-weapon-epic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-weapon-epic.png -------------------------------------------------------------------------------- /assets/rarity-weapon-legendary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-weapon-legendary.png -------------------------------------------------------------------------------- /assets/rarity-weapon-rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/rarity-weapon-rare.png -------------------------------------------------------------------------------- /assets/sPreload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/sPreload.jpg -------------------------------------------------------------------------------- /assets/set-boomsday.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-boomsday.png -------------------------------------------------------------------------------- /assets/set-brm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-brm.png -------------------------------------------------------------------------------- /assets/set-dalaran.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-dalaran.png -------------------------------------------------------------------------------- /assets/set-expert1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-expert1.png -------------------------------------------------------------------------------- /assets/set-gangs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-gangs.png -------------------------------------------------------------------------------- /assets/set-gilneas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-gilneas.png -------------------------------------------------------------------------------- /assets/set-gvg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-gvg.png -------------------------------------------------------------------------------- /assets/set-hof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-hof.png -------------------------------------------------------------------------------- /assets/set-icecrown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-icecrown.png -------------------------------------------------------------------------------- /assets/set-kara.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-kara.png -------------------------------------------------------------------------------- /assets/set-loe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-loe.png -------------------------------------------------------------------------------- /assets/set-lootapalooza.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-lootapalooza.png -------------------------------------------------------------------------------- /assets/set-naxx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-naxx.png -------------------------------------------------------------------------------- /assets/set-og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-og.png -------------------------------------------------------------------------------- /assets/set-tgt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-tgt.png -------------------------------------------------------------------------------- /assets/set-troll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-troll.png -------------------------------------------------------------------------------- /assets/set-ungoro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/set-ungoro.png -------------------------------------------------------------------------------- /assets/silence-x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/silence-x.png -------------------------------------------------------------------------------- /assets/wPreload.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HearthSim/Sunwell/e6c4dc118a76e5b15e65d1897625c4f266c4d44e/assets/wPreload.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sunwell", 3 | "version": "0.10.0", 4 | "description": "Canvas-based high quality Hearthstone card renderer", 5 | "keywords": ["hearthstone"], 6 | "homepage": "https://github.com/HearthSim/Sunwell#readme", 7 | "bugs": { 8 | "url": "https://github.com/HearthSim/Sunwell/issues" 9 | }, 10 | "license": "MIT", 11 | "author": "Jerome Leclanche ", 12 | "contributors": ["Benedict Etzel "], 13 | "main": "dist/sunwell.node.js", 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/HearthSim/Sunwell.git" 17 | }, 18 | "scripts": { 19 | "prepublishOnly": "yarn build", 20 | "build": "rollup -c && PLATFORM=web rollup -c", 21 | "format": "yarn format:prettier", 22 | "format:prettier": "prettier --write *.{js,json} src/**/*.{ts,tsx,js}", 23 | "lint": "yarn lint:prettier", 24 | "lint:prettier": "prettier -l *.{js,json} src/**/*.{ts,tsx,js}", 25 | "qa": "tslint src/*.ts src/**/*.ts", 26 | "dev": "tsc --watch" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "^8.0.13", 30 | "prettier": "^1.9.1", 31 | "request": "^2.81.0", 32 | "rollup": "^0.50.0", 33 | "rollup-plugin-cleanup": "^1.0.1", 34 | "rollup-plugin-commonjs": "^8.0.2", 35 | "rollup-plugin-jscc": "^0.3.3", 36 | "rollup-plugin-node-resolve": "^3.0.0", 37 | "rollup-plugin-replace": "^2.0.0", 38 | "rollup-plugin-typescript": "^0.8.1", 39 | "rollup-plugin-uglify": "^2.0.1", 40 | "ts-loader": "^2.0.1", 41 | "tslint": "^5.5.0", 42 | "typescript": "^2.4.1", 43 | "webpack": "^3.2.0", 44 | "webpack-node-externals": "^1.6.0" 45 | }, 46 | "dependencies": { 47 | "argparse": "^1.0.9", 48 | "canvas": "^2.0.0-alpha.12", 49 | "chars": "^2.3.0", 50 | "linebreak": "^0.3.0", 51 | "mkdirp": "^0.5.1", 52 | "promise": "^8.0.1" 53 | }, 54 | "prettier": { 55 | "printWidth": 100, 56 | "useTabs": true, 57 | "tabWidth": 4, 58 | "bracketSpacing": false, 59 | "trailingComma": "es5" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from "rollup-plugin-typescript"; 2 | import resolve from "rollup-plugin-node-resolve"; 3 | import commonjs from "rollup-plugin-commonjs"; 4 | import jscc from "rollup-plugin-jscc"; 5 | import cleanup from "rollup-plugin-cleanup"; 6 | import uglify from "rollup-plugin-uglify"; 7 | 8 | const ExternalModulesList = [].concat( 9 | require("builtin-modules"), 10 | Object.keys(require("./package.json").dependencies) 11 | ); 12 | 13 | const PLATFORM = ["web", "node"].includes(process.env.PLATFORM) ? process.env.PLATFORM : "node"; 14 | const PRODUCTION = process.env.NODE_ENV === "production"; 15 | export default { 16 | input: "src/Sunwell.ts", 17 | output: { 18 | format: "cjs", 19 | file: `dist/sunwell.${PLATFORM}${PRODUCTION ? ".min" : ""}.js`, 20 | }, 21 | external: ExternalModulesList, 22 | name: "Sunwell", 23 | plugins: [ 24 | jscc({ 25 | values: { 26 | _PLATFORM: PLATFORM, 27 | }, 28 | extensions: [".js", ".ts"], 29 | }), 30 | typescript({ 31 | typescript: require("typescript"), 32 | }), 33 | resolve(), 34 | commonjs({ 35 | exclude: ExternalModulesList, 36 | ignoreGlobal: true, 37 | extensions: [".js", ".ts"], 38 | }), 39 | cleanup(), 40 | PRODUCTION ? uglify() : undefined, 41 | ].filter(p => p), 42 | }; 43 | -------------------------------------------------------------------------------- /scripts/sunwell-card-renderer.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const mkdirp = require("mkdirp"); 4 | const request = require("request"); 5 | const {ArgumentParser} = require("argparse"); 6 | const Canvas = require("canvas"); 7 | const Promise = require("promise"); 8 | const Sunwell = require("../dist/sunwell.node"); 9 | 10 | function renderCard(sunwell, card, filePath, resolution, premium) { 11 | if (!card.type || !card.cardClass) { 12 | console.log("Skipping", card.id, "(no card to render)"); 13 | return; 14 | } 15 | 16 | if ( 17 | card.type !== "MINION" && 18 | card.type !== "SPELL" && 19 | card.type !== "WEAPON" && 20 | card.type !== "HERO" && 21 | card.type !== "HERO_POWER" 22 | ) { 23 | console.log("Skipping", card.id, "(not a renderable card)"); 24 | return; 25 | } 26 | 27 | if (!fs.existsSync(card.texture)) { 28 | console.log("Skipping", card.id, "(texture not found)", card.texture); 29 | return; 30 | } 31 | 32 | // Turn the texture into an image object 33 | fs.readFile(card.texture, function(err, data) { 34 | if (err) { 35 | throw err; 36 | } 37 | card.texture = new Canvas.Image(); 38 | card.texture.src = data; 39 | 40 | let callback = function(canvas) { 41 | let out = fs.createWriteStream(filePath); 42 | let stream = canvas.pngStream(); 43 | stream.on("data", chunk => { 44 | out.write(chunk); 45 | }); 46 | stream.on("end", chunk => { 47 | console.log("Done rendering", filePath); 48 | }); 49 | stream.on("error", chunk => { 50 | console.log("Error writing chunk", filePath); 51 | }); 52 | }; 53 | sunwell.createCard(card, resolution, premium, null, callback); 54 | }); 55 | } 56 | 57 | function drawFromJSON(sunwell, body, textureDir, outputDir, resolution) { 58 | const data = JSON.parse(body); 59 | drawFromData(sunwell, data, textureDir, outputDir, resolution); 60 | } 61 | 62 | function drawFromData(sunwell, data, textureDir, outputDir, resolution, premium, overwrite) { 63 | for (let i in data) { 64 | var card = data[i]; 65 | card.key = i; 66 | var renderName = card.id + (premium ? "_premium" : "") + ".png"; 67 | var renderPath = path.join(outputDir, renderName); 68 | var textureName = card.id + ".jpg"; 69 | var texturePath = path.join(textureDir, textureName); 70 | 71 | if (!fs.existsSync(renderPath) || overwrite) { 72 | card.texture = texturePath; 73 | renderCard(sunwell, card, renderPath, resolution, premium); 74 | } 75 | } 76 | } 77 | 78 | const fonts = { 79 | "belwe/belwe-extrabold.ttf": {family: "Belwe"}, 80 | "franklin-gothic-bold/franklingothic-demicd.ttf": { 81 | family: "Franklin Gothic Bold", 82 | weight: "bold", 83 | }, 84 | "franklin-gothic-italic/franklingothic-medcdit.ttf": { 85 | family: "Franklin Gothic Italic", 86 | style: "italic", 87 | }, 88 | "franklin-gothic/franklingothic-medcd.ttf": {family: "Franklin Gothic"}, 89 | }; 90 | 91 | function main() { 92 | var p = new ArgumentParser({ 93 | description: "Generate card renders", 94 | }); 95 | p.addArgument("file", {nargs: "+"}); 96 | p.addArgument("--assets-dir", {defaultValue: path.resolve("..", "src", "assets")}); 97 | p.addArgument("--font-dir", {defaultValue: path.resolve("fonts")}); 98 | p.addArgument("--output-dir", {defaultValue: path.resolve("out")}); 99 | p.addArgument("--texture-dir", {defaultValue: path.resolve("textures")}); 100 | p.addArgument("--resolution", {type: "int", defaultValue: 512}); 101 | p.addArgument("--premium", {action: "storeTrue"}); 102 | p.addArgument("--overwrite", {action: "storeTrue"}); 103 | p.addArgument("--only", {type: "string", defaultValue: ""}); 104 | p.addArgument("--debug", {action: "storeTrue"}); 105 | var args = p.parseArgs(); 106 | var sunwell = new Sunwell({ 107 | titleFont: "Belwe", 108 | bodyFontBold: "Franklin Gothic Bold", 109 | bodyFontItalic: "Franklin Gothic Italic", 110 | bodyFontBoldItalic: "Franklin Gothic Bold Italic", 111 | bodyFontSize: 38, 112 | bodyLineHeight: 40, 113 | bodyFontOffset: {x: 0, y: 26}, 114 | assetFolder: path.resolve(args.assets_dir) + "/", 115 | debug: args.debug, 116 | // platform: new NodePlatform(), 117 | cacheSkeleton: false, 118 | }); 119 | 120 | for (let key of Object.keys(fonts)) { 121 | let font = fonts[key]; 122 | let fontPath = path.join(args.font_dir, key); 123 | if (!fs.existsSync(fontPath)) { 124 | console.log("WARNING: Font not found:", fontPath); 125 | } else { 126 | Canvas.registerFont(fontPath, font); 127 | } 128 | } 129 | 130 | const only = args.only.split(",").map(id => id.trim()); 131 | 132 | let outdir = path.resolve(args.output_dir); 133 | mkdirp.sync(outdir); 134 | 135 | for (let file of args.file) { 136 | fs.readFile(file, function(err, body) { 137 | if (err) { 138 | console.log("Error reading", file, err); 139 | return; 140 | } 141 | let data = JSON.parse(body); 142 | if (only.length && only[0].length) { 143 | data = data.filter(card => only.indexOf(card.id) !== -1); 144 | } 145 | drawFromData( 146 | sunwell, 147 | data, 148 | path.resolve(args.texture_dir), 149 | outdir, 150 | args.resolution, 151 | args.premium, 152 | args.overwrite 153 | ); 154 | }); 155 | } 156 | } 157 | 158 | main(); 159 | -------------------------------------------------------------------------------- /src/Card.ts: -------------------------------------------------------------------------------- 1 | import CardDef from "./CardDef"; 2 | import AttackGem from "./Components/AttackGem"; 3 | import BodyText from "./Components/BodyText"; 4 | import CardArt from "./Components/CardArt"; 5 | import CardFrame from "./Components/CardFrame"; 6 | import CostGem from "./Components/CostGem"; 7 | import EliteDragon from "./Components/EliteDragon"; 8 | import HealthGem from "./Components/HealthGem"; 9 | import MultiClassBanner from "./Components/MultiClassBanner"; 10 | import NameBanner from "./Components/NameBanner"; 11 | import RaceBanner from "./Components/RaceBanner"; 12 | import RarityGem from "./Components/RarityGem"; 13 | import Watermark from "./Components/Watermark"; 14 | import {CardClass, CardSet, MultiClassGroup, Rarity} from "./Enums"; 15 | import {getCardFrameClass, getNumberStyle, getRaceText, getRarityGem} from "./helpers"; 16 | import {ICoords, IPoint} from "./interfaces"; 17 | import Sunwell from "./Sunwell"; 18 | 19 | const ReferenceWidth = 670; 20 | 21 | export default abstract class Card { 22 | public cardDef: CardDef; 23 | public target; 24 | public texture; 25 | public canvas: HTMLCanvasElement; 26 | public raceText: string; 27 | public language: string; 28 | public costColor: string; 29 | public attackColor: string; 30 | public healthColor: string; 31 | public width: number; 32 | public key: number; 33 | public opposing: boolean; 34 | public attackGem: AttackGem; 35 | public bodyText: BodyText; 36 | public cardArt: CardArt; 37 | public cardFrame: CardFrame; 38 | public costGem: CostGem; 39 | public eliteDragon: EliteDragon; 40 | public healthGem: HealthGem; 41 | public multiClassBanner: MultiClassBanner; 42 | public raceBanner: RaceBanner; 43 | public nameBanner: NameBanner; 44 | public rarityGem: RarityGem; 45 | public watermark: Watermark; 46 | public eliteDragonAsset: string = ""; 47 | public eliteDragonCoords: ICoords = null; 48 | public raceBannerAsset: string = ""; 49 | public raceTextCoords: ICoords = {dx: 337, dy: 829}; 50 | public raceBannerCoords: ICoords = { 51 | dx: 129, 52 | dy: 791, 53 | dWidth: 408, 54 | dHeight: 69, 55 | sWidth: 408, 56 | sHeight: 69, 57 | }; 58 | 59 | public abstract baseCardFrameAsset: string; 60 | public abstract baseCardFrameCoords: ICoords; 61 | public abstract baseRarityGemAsset: string; 62 | public abstract bodyTextColor: string; 63 | public abstract bodyTextCoords: ICoords; 64 | public abstract nameBannerAsset: string; 65 | public abstract nameBannerCoords: ICoords; 66 | public abstract rarityGemCoords: ICoords; 67 | public abstract nameTextCurve: { 68 | pathMiddle: number; 69 | maxWidth: number; 70 | curve: IPoint[]; 71 | }; 72 | public abstract artClipPolygon: IPoint[]; 73 | public abstract artCoords: ICoords; 74 | public abstract cardFoundationAsset: string; 75 | public abstract cardFoundationCoords: ICoords; 76 | public abstract premium: boolean; 77 | 78 | private callback: (HTMLCanvasElement) => void; 79 | private cacheKey: number; 80 | private propsJson: string; 81 | private sunwell: Sunwell; 82 | 83 | constructor(sunwell: Sunwell, props) { 84 | this.sunwell = sunwell; 85 | if (!props) { 86 | throw new Error("No card properties given"); 87 | } 88 | 89 | this.cardDef = new CardDef(props); 90 | this.language = props.language || "enUS"; 91 | 92 | // Sets the player or opponent HeroPower texture 93 | this.opposing = props.opposing || false; 94 | 95 | this.raceText = getRaceText(this.cardDef.race, this.cardDef.type, this.language); 96 | this.costColor = getNumberStyle(props.costStyle); 97 | this.attackColor = getNumberStyle(props.costStyle); 98 | this.healthColor = getNumberStyle(props.healthStyle); 99 | this.texture = props.texture; 100 | this.propsJson = JSON.stringify(props); 101 | 102 | this.attackGem = new AttackGem(sunwell, this); 103 | this.bodyText = new BodyText(sunwell, this); 104 | this.cardArt = new CardArt(sunwell, this); 105 | this.cardFrame = new CardFrame(sunwell, this); 106 | this.costGem = new CostGem(sunwell, this); 107 | this.eliteDragon = new EliteDragon(sunwell, this); 108 | this.healthGem = new HealthGem(sunwell, this); 109 | this.multiClassBanner = new MultiClassBanner(sunwell, this); 110 | this.nameBanner = new NameBanner(sunwell, this); 111 | this.raceBanner = new RaceBanner(sunwell, this); 112 | this.rarityGem = new RarityGem(sunwell, this); 113 | this.watermark = new Watermark(sunwell, this); 114 | } 115 | 116 | public abstract getWatermarkCoords(): ICoords; 117 | 118 | public getAttackGemAsset(): string { 119 | return ""; 120 | } 121 | 122 | public getAttackGemCoords(): ICoords { 123 | return null; 124 | } 125 | 126 | public getAttackTextCoords(): ICoords { 127 | return null; 128 | } 129 | 130 | public getCostGemCoords(): ICoords { 131 | if (this.cardDef.costsHealth) { 132 | return {dx: 43, dy: 58}; 133 | } else { 134 | return {dx: 47, dy: 105}; 135 | } 136 | } 137 | 138 | public getCostTextCoords(): ICoords { 139 | return {dx: 115, dy: 174}; 140 | } 141 | 142 | public getCostGemAsset(): string { 143 | if (this.cardDef.costsHealth) { 144 | return "cost-health"; 145 | } else { 146 | return "cost-mana"; 147 | } 148 | } 149 | 150 | public getHealthGemAsset(): string { 151 | return ""; 152 | } 153 | 154 | public getHealthGemCoords(): ICoords { 155 | return null; 156 | } 157 | 158 | public getHealthTextCoords(): ICoords { 159 | return null; 160 | } 161 | 162 | public initRender(width: number, target, callback?: (HTMLCanvasElement) => void): void { 163 | this.width = width; 164 | this.target = target; 165 | this.callback = callback; 166 | this.cacheKey = this.checksum(); 167 | this.key = this.cacheKey; 168 | } 169 | 170 | public getAssetsToLoad(): string[] { 171 | const assetsToCheck = [this.cardFoundationAsset]; 172 | const assetsToLoad: string[] = []; 173 | for (const asset of assetsToCheck) { 174 | if (asset) { 175 | assetsToLoad.push(asset); 176 | } 177 | } 178 | 179 | assetsToLoad.push(...this.attackGem.assets()); 180 | assetsToLoad.push(...this.nameBanner.assets()); 181 | assetsToLoad.push(...this.cardFrame.assets()); 182 | assetsToLoad.push(...this.costGem.assets()); 183 | assetsToLoad.push(...this.eliteDragon.assets()); 184 | assetsToLoad.push(...this.healthGem.assets()); 185 | assetsToLoad.push(...this.multiClassBanner.assets()); 186 | assetsToLoad.push(...this.raceBanner.assets()); 187 | assetsToLoad.push(...this.rarityGem.assets()); 188 | assetsToLoad.push(...this.watermark.assets()); 189 | assetsToLoad.push(...this.eliteDragon.assets()); 190 | 191 | if (this.cardDef.silenced) { 192 | assetsToLoad.push("silence-x"); 193 | } 194 | 195 | return assetsToLoad; 196 | } 197 | 198 | public getCardArtTexture(): HTMLImageElement | HTMLCanvasElement { 199 | if (!this.texture) { 200 | this.sunwell.log("No card texture specified. Creating empty texture."); 201 | return this.sunwell.getBuffer(1024, 1024); 202 | } else if (this.texture instanceof this.sunwell.platform.Image) { 203 | return this.texture; 204 | } else if (typeof this.texture === "string") { 205 | return this.sunwell.assets[this.texture]; 206 | } else { 207 | const t = this.sunwell.getBuffer(this.texture.crop.w, this.texture.crop.h); 208 | const tCtx = t.getContext("2d"); 209 | tCtx.drawImage( 210 | this.texture.image, 211 | this.texture.crop.x, 212 | this.texture.crop.y, 213 | this.texture.crop.w, 214 | this.texture.crop.h, 215 | 0, 216 | 0, 217 | t.width, 218 | t.height 219 | ); 220 | return t; 221 | } 222 | } 223 | 224 | public draw(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D): void { 225 | const ratio = this.width / ReferenceWidth; 226 | 227 | const drawTimeout = setTimeout(() => { 228 | this.sunwell.error("Drawing timed out", this.cardDef.name); 229 | }, this.sunwell.options.drawTimeout); 230 | 231 | context.save(); 232 | context.clearRect(0, 0, canvas.width, canvas.height); 233 | 234 | // >>>>> Begin Skeleton drawing 235 | if (this.sunwell.renderCache[this.cacheKey]) { 236 | this.sunwell.log("Skipping skeleton draw"); 237 | context.drawImage(this.sunwell.renderCache[this.cacheKey], 0, 0); 238 | } else { 239 | if (this.cardFoundationAsset) { 240 | this.drawCardFoundationAsset(context, ratio); 241 | } 242 | 243 | this.cardArt.render(context, ratio); 244 | this.cardFrame.render(context, ratio); 245 | this.rarityGem.render(context, ratio); 246 | this.eliteDragon.render(context, ratio); 247 | this.nameBanner.render(context, ratio); 248 | this.raceBanner.render(context, ratio); 249 | this.attackGem.render(context, ratio); 250 | this.multiClassBanner.render(context, ratio); 251 | this.costGem.render(context, ratio); 252 | this.healthGem.render(context, ratio); 253 | this.watermark.render(context, ratio); 254 | 255 | if (this.sunwell.options.cacheSkeleton) { 256 | const cacheImage = new this.sunwell.platform.Image(); 257 | cacheImage.src = canvas.toDataURL(); 258 | this.sunwell.renderCache[this.cacheKey] = cacheImage; 259 | } 260 | } 261 | 262 | this.bodyText.render(context, ratio); 263 | 264 | if (this.cardDef.silenced) { 265 | this.sunwell.drawImage(context, "silence-x", {dx: 166, dy: 584, ratio: ratio}); 266 | } 267 | 268 | context.restore(); 269 | clearTimeout(drawTimeout); 270 | 271 | if (this.callback) { 272 | this.callback(canvas); 273 | } 274 | 275 | if (this.target) { 276 | this.target.src = canvas.toDataURL(); 277 | } 278 | } 279 | 280 | public getBodyText(): string { 281 | return this.cardDef.collectionText || this.cardDef.text; 282 | } 283 | 284 | public drawCardFoundationAsset(context: CanvasRenderingContext2D, ratio: number): void { 285 | const coords = this.cardFoundationCoords; 286 | coords.ratio = ratio; 287 | this.sunwell.drawImage(context, this.cardFoundationAsset, coords); 288 | } 289 | 290 | public getCardFrameAsset(): string { 291 | const cardClass = getCardFrameClass(this.cardDef.cardClass); 292 | return this.baseCardFrameAsset + CardClass[cardClass].toLowerCase(); 293 | } 294 | 295 | public getEliteDragonAsset(): string { 296 | if (this.cardDef.elite) { 297 | return this.eliteDragonAsset; 298 | } else { 299 | return ""; 300 | } 301 | } 302 | 303 | public getMultiClassBannerAsset(): string { 304 | if (this.cardDef.multiClassGroup) { 305 | return "multi-" + MultiClassGroup[this.cardDef.multiClassGroup].toLowerCase(); 306 | } else { 307 | return ""; 308 | } 309 | } 310 | 311 | public getRarityGemAsset(): string { 312 | const rarity = getRarityGem(this.cardDef.rarity, this.cardDef.cardSet, this.cardDef.type); 313 | if (rarity) { 314 | return this.baseRarityGemAsset + Rarity[rarity].toLowerCase(); 315 | } 316 | return ""; 317 | } 318 | 319 | public getWatermarkAsset(): string { 320 | switch (this.cardDef.cardSet) { 321 | case CardSet.EXPERT1: 322 | case CardSet.NAXX: 323 | case CardSet.GVG: 324 | case CardSet.BRM: 325 | case CardSet.TGT: 326 | case CardSet.LOE: 327 | case CardSet.OG: 328 | case CardSet.KARA: 329 | case CardSet.GANGS: 330 | case CardSet.UNGORO: 331 | case CardSet.ICECROWN: 332 | case CardSet.HOF: 333 | case CardSet.LOOTAPALOOZA: 334 | case CardSet.GILNEAS: 335 | case CardSet.BOOMSDAY: 336 | case CardSet.TROLL: 337 | case CardSet.DALARAN: 338 | return "set-" + CardSet[this.cardDef.cardSet].toLowerCase(); 339 | default: 340 | return ""; 341 | } 342 | } 343 | 344 | private checksum(): number { 345 | const s = this.propsJson + this.width + this.premium; 346 | const length = s.length; 347 | let chk = 0; 348 | for (let i = 0; i < length; i++) { 349 | const char = s.charCodeAt(i); 350 | chk = (chk << 5) - chk + char; 351 | chk |= 0; 352 | } 353 | 354 | return chk; 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/CardDef.ts: -------------------------------------------------------------------------------- 1 | import {CardClass, CardSet, CardType, MultiClassGroup, Race, Rarity} from "./Enums"; 2 | import {cleanEnum} from "./helpers"; 3 | 4 | export default class CardDef { 5 | public attack: number; 6 | public armor: number; 7 | public cardClass: CardClass; 8 | public cardSet: CardSet; 9 | public collectionText: string; 10 | public cost: number; 11 | public costsHealth: boolean; 12 | public elite: boolean; 13 | public health: number; 14 | public hideStats: boolean; 15 | public id: string; 16 | public name: string; 17 | public multiClassGroup: MultiClassGroup; 18 | public rarity: Rarity; 19 | public race: Race; 20 | public silenced: boolean; 21 | public text: string; 22 | public type: CardType; 23 | 24 | constructor(props: any) { 25 | this.attack = props.attack || 0; 26 | this.armor = props.armor || 0; 27 | this.cardClass = cleanEnum(props.cardClass, CardClass) as CardClass; 28 | this.cardSet = cleanEnum(props.set, CardSet) as CardSet; 29 | this.cost = props.cost || 0; 30 | this.costsHealth = props.costsHealth || false; 31 | this.elite = props.elite || false; 32 | this.health = props.health || 0; 33 | this.hideStats = props.hideStats || false; 34 | this.multiClassGroup = cleanEnum(props.multiClassGroup, MultiClassGroup) as MultiClassGroup; 35 | this.name = props.name || ""; 36 | this.race = cleanEnum(props.race, Race) as Race; 37 | this.rarity = cleanEnum(props.rarity, Rarity) as Rarity; 38 | this.silenced = props.silenced || false; 39 | this.type = cleanEnum(props.type, CardType) as CardType; 40 | 41 | if (this.type === CardType.WEAPON && props.durability) { 42 | // Weapons alias health to durability 43 | this.health = props.durability; 44 | } else if (this.type === CardType.HERO && props.armor) { 45 | // Hero health gem is Armor 46 | this.health = props.armor; 47 | } 48 | 49 | this.collectionText = props.collectionText || ""; 50 | this.text = props.text || ""; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Components/AttackGem.ts: -------------------------------------------------------------------------------- 1 | import Card from "../Card"; 2 | import Sunwell from "../Sunwell"; 3 | import Gem from "./Gem"; 4 | 5 | export default class AttackGem extends Gem { 6 | constructor(sunwell: Sunwell, parent: Card) { 7 | super(sunwell, parent); 8 | this.showGem = !parent.cardDef.hideStats; 9 | this.showText = !parent.cardDef.hideStats; 10 | this.gemAsset = parent.getAttackGemAsset(); 11 | this.gemCoords = parent.getAttackGemCoords(); 12 | this.text = parent.cardDef.attack.toString(); 13 | this.textColor = parent.attackColor; 14 | this.textCoords = parent.getAttackTextCoords(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Components/BodyText.ts: -------------------------------------------------------------------------------- 1 | import chars from "chars"; 2 | import LineBreaker from "linebreak"; 3 | import {CardType} from "../Enums"; 4 | import {contextBoundingBox} from "../helpers"; 5 | import Component from "./Component"; 6 | 7 | const CTRL_MANUAL_LINEBREAKS = "\x10"; 8 | const CTRL_BOLD_START = "\x11"; 9 | const CTRL_BOLD_END = "\x12"; 10 | const CTRL_ITALIC_START = "\x13"; 11 | const CTRL_ITALIC_END = "\x14"; 12 | 13 | /** 14 | * Finishes a text line and starts a new one. 15 | */ 16 | function finishLine( 17 | bufferTextCtx: CanvasRenderingContext2D, 18 | bufferRow: HTMLCanvasElement, 19 | bufferRowCtx: CanvasRenderingContext2D, 20 | xPos: number, 21 | yPos: number, 22 | totalWidth: number 23 | ): [number, number] { 24 | let xCalc = totalWidth / 2 - xPos / 2; 25 | 26 | if (xCalc < 0) { 27 | xCalc = 0; 28 | } 29 | 30 | if (xPos > 0 && bufferRow.width > 0) { 31 | bufferTextCtx.drawImage( 32 | bufferRow, 33 | 0, 34 | 0, 35 | xPos > bufferRow.width ? bufferRow.width : xPos, 36 | bufferRow.height, 37 | xCalc, 38 | yPos, 39 | Math.min(xPos, bufferRow.width), 40 | bufferRow.height 41 | ); 42 | } 43 | 44 | xPos = 5; 45 | yPos += bufferRow.height; 46 | bufferRowCtx.clearRect(0, 0, bufferRow.width, bufferRow.height); 47 | 48 | return [xPos, yPos]; 49 | } 50 | 51 | export default class BodyText extends Component { 52 | public render(context: CanvasRenderingContext2D, ratio: number): void { 53 | this.drawBodyText(context, ratio, false); 54 | } 55 | 56 | private parseBodyText(text: string): string { 57 | text = text 58 | .replace(/\[x\]/g, CTRL_MANUAL_LINEBREAKS) 59 | .replace(//g, CTRL_BOLD_START) 60 | .replace(/<\/b>/g, CTRL_BOLD_END) 61 | .replace(//g, CTRL_ITALIC_START) 62 | .replace(/<\/i>/g, CTRL_ITALIC_END); 63 | 64 | const pluralRegex = /(\d+)(.+?)\|4\((.+?),(.+?)\)/g; 65 | let plurals: RegExpExecArray; 66 | while ((plurals = pluralRegex.exec(text)) !== null) { 67 | text = text.replace( 68 | plurals[0], 69 | plurals[1] + plurals[2] + (parseInt(plurals[1], 10) === 1 ? plurals[3] : plurals[4]) 70 | ); 71 | } 72 | 73 | let spellDmg: RegExpExecArray; 74 | const spellDamageRegex = /\$(\d+)/; 75 | while ((spellDmg = spellDamageRegex.exec(text)) !== null) { 76 | text = text.replace(spellDmg[0], spellDmg[1]); 77 | } 78 | 79 | let spellHeal: RegExpExecArray; 80 | const spellHealRegex = /#(\d+)/; 81 | while ((spellHeal = spellHealRegex.exec(text)) !== null) { 82 | text = text.replace(spellHeal[0], spellHeal[1]); 83 | } 84 | 85 | return text; 86 | } 87 | 88 | private drawBodyText( 89 | context: CanvasRenderingContext2D, 90 | ratio: number, 91 | forceSmallerFirstLine: boolean 92 | ): void { 93 | let xPos = 0; 94 | let yPos = 0; 95 | let italic = 0; 96 | let bold = 0; 97 | let lineCount = 0; 98 | let justLineBreak: boolean; 99 | // size of the description text box 100 | const bodyWidth = this.parent.bodyTextCoords.dWidth; 101 | const bodyHeight = this.parent.bodyTextCoords.dHeight; 102 | // center of description box (x, y) 103 | const centerLeft = this.parent.bodyTextCoords.dx + bodyWidth / 2; 104 | const centerTop = this.parent.bodyTextCoords.dy + bodyHeight / 2; 105 | 106 | const bodyText = this.parseBodyText(this.parent.getBodyText()); 107 | this.sunwell.log("Body text", bodyText); 108 | 109 | const words: string[] = []; 110 | const breaker = new LineBreaker(bodyText); 111 | let last = 0; 112 | let bk; 113 | while ((bk = breaker.nextBreak())) { 114 | words.push(bodyText.slice(last, bk.position).replace("\n", "")); 115 | last = bk.position; 116 | if (bk.required) { 117 | words.push("\n"); 118 | } 119 | } 120 | this.sunwell.log("Words", words); 121 | 122 | const bufferText = this.sunwell.getBuffer(bodyWidth, bodyHeight, true); 123 | const bufferTextCtx = bufferText.getContext("2d"); 124 | bufferTextCtx.fillStyle = this.parent.bodyTextColor; 125 | 126 | let fontSize = this.sunwell.options.bodyFontSize; 127 | let lineHeight = this.sunwell.options.bodyLineHeight; 128 | const totalLength = bodyText.length; 129 | this.sunwell.log("Length of text is " + totalLength); 130 | 131 | const bufferRow = this.sunwell.getBuffer(bufferText.width, lineHeight, true); 132 | const bufferRowCtx = bufferRow.getContext("2d"); 133 | bufferRowCtx.fillStyle = this.parent.bodyTextColor; 134 | // bufferRowCtx.textBaseline = this.sunwell.options.bodyBaseline; 135 | 136 | // XXX: manual breaks can be anywhere, not just at the beginning 137 | // cf. AT_132 locale ruRU 138 | const manualBreak = bodyText.indexOf(CTRL_MANUAL_LINEBREAKS) === 0; 139 | if (manualBreak) { 140 | let maxWidth = 0; 141 | bufferRowCtx.font = this.getFontMaterial(fontSize, false, false); 142 | bodyText.split("\n").forEach((line: string) => { 143 | const width = this.getLineWidth(bufferRowCtx, fontSize, line); 144 | if (width > maxWidth) { 145 | maxWidth = width; 146 | } 147 | }); 148 | if (maxWidth > bufferText.width) { 149 | const r = bufferText.width / maxWidth; 150 | fontSize *= r; 151 | lineHeight *= r; 152 | } 153 | } else { 154 | if (totalLength >= 65) { 155 | fontSize = this.sunwell.options.bodyFontSize * 0.95; 156 | lineHeight = this.sunwell.options.bodyLineHeight * 0.95; 157 | } 158 | 159 | if (totalLength >= 80) { 160 | fontSize = this.sunwell.options.bodyFontSize * 0.9; 161 | lineHeight = this.sunwell.options.bodyLineHeight * 0.9; 162 | } 163 | 164 | if (totalLength >= 100) { 165 | fontSize = this.sunwell.options.bodyFontSize * 0.8; 166 | lineHeight = this.sunwell.options.bodyLineHeight * 0.8; 167 | } 168 | 169 | if (totalLength >= 120) { 170 | fontSize = this.sunwell.options.bodyFontSize * 0.62; 171 | lineHeight = this.sunwell.options.bodyLineHeight * 0.8; 172 | } 173 | } 174 | 175 | bufferRowCtx.font = this.getFontMaterial(fontSize, !!bold, !!italic); 176 | bufferRow.height = lineHeight; 177 | 178 | let smallerFirstLine = false; 179 | if ( 180 | forceSmallerFirstLine || 181 | (totalLength >= 75 && this.parent.cardDef.type === CardType.SPELL) 182 | ) { 183 | smallerFirstLine = true; 184 | } 185 | 186 | for (const word of words) { 187 | const cleanWord = word.trim().replace(/<((?!>).)*>/g, ""); 188 | 189 | const width = bufferRowCtx.measureText(cleanWord).width; 190 | this.sunwell.log("Next word:", word); 191 | 192 | if ( 193 | !manualBreak && 194 | (xPos + width > bufferRow.width || 195 | (smallerFirstLine && xPos + width > bufferRow.width * 0.8)) && 196 | !justLineBreak 197 | ) { 198 | this.sunwell.log(xPos + width, ">", bufferRow.width); 199 | this.sunwell.log("Calculated line break"); 200 | smallerFirstLine = false; 201 | justLineBreak = true; 202 | lineCount++; 203 | [xPos, yPos] = finishLine( 204 | bufferTextCtx, 205 | bufferRow, 206 | bufferRowCtx, 207 | xPos, 208 | yPos, 209 | bufferText.width 210 | ); 211 | } 212 | 213 | if (word === "\n") { 214 | this.sunwell.log("Manual line break"); 215 | lineCount++; 216 | [xPos, yPos] = finishLine( 217 | bufferTextCtx, 218 | bufferRow, 219 | bufferRowCtx, 220 | xPos, 221 | yPos, 222 | bufferText.width 223 | ); 224 | 225 | justLineBreak = true; 226 | smallerFirstLine = false; 227 | continue; 228 | } 229 | 230 | justLineBreak = false; 231 | 232 | for (const char of chars(word)) { 233 | switch (char) { 234 | case CTRL_MANUAL_LINEBREAKS: 235 | // TODO: Turn on manual linebreaking 236 | continue; 237 | case CTRL_BOLD_START: 238 | bold += 1; 239 | continue; 240 | case CTRL_BOLD_END: 241 | bold -= 1; 242 | continue; 243 | case CTRL_ITALIC_START: 244 | italic += 1; 245 | continue; 246 | case CTRL_ITALIC_END: 247 | italic -= 1; 248 | continue; 249 | } 250 | 251 | // TODO investigate why the following two properites are being reset, for web 252 | // likely something to do with getLineWidth() 253 | bufferRowCtx.fillStyle = this.parent.bodyTextColor; 254 | // move to here from pr.token block above, 255 | // text without markup ends up being default font otherwise 256 | bufferRowCtx.font = this.getFontMaterial(fontSize, !!bold, !!italic); 257 | 258 | bufferRowCtx.fillText( 259 | char, 260 | xPos + this.sunwell.options.bodyFontOffset.x, 261 | this.sunwell.options.bodyFontOffset.y 262 | ); 263 | 264 | xPos += bufferRowCtx.measureText(char).width; 265 | } 266 | const em = bufferRowCtx.measureText("M").width; 267 | xPos += 0.275 * em; 268 | } 269 | 270 | lineCount++; 271 | finishLine(bufferTextCtx, bufferRow, bufferRowCtx, xPos, yPos, bufferText.width); 272 | 273 | this.sunwell.freeBuffer(bufferRow); 274 | 275 | if (this.parent.cardDef.type === CardType.SPELL && lineCount === 4) { 276 | if (!smallerFirstLine && !forceSmallerFirstLine) { 277 | this.drawBodyText(context, ratio, true); 278 | return; 279 | } 280 | } 281 | 282 | const b = contextBoundingBox(bufferTextCtx); 283 | 284 | b.h = Math.ceil(b.h / bufferRow.height) * bufferRow.height; 285 | 286 | context.drawImage( 287 | bufferText, 288 | b.x, 289 | b.y - 2, 290 | b.w, 291 | b.h, 292 | (centerLeft - b.w / 2) * ratio, 293 | (centerTop - b.h / 2) * ratio, 294 | b.w * ratio, 295 | (b.h + 2) * ratio 296 | ); 297 | 298 | this.sunwell.freeBuffer(bufferText); 299 | } 300 | 301 | private getFontMaterial(size: number, bold: boolean, italic: boolean): string { 302 | let font: string; 303 | const weight = bold ? "bold" : ""; 304 | const style = italic ? "italic" : ""; 305 | let fontSize = `${size}px`; 306 | 307 | if (this.sunwell.options.bodyLineStyle !== "") { 308 | fontSize = `${fontSize}/${this.sunwell.options.bodyLineStyle}`; 309 | } 310 | 311 | if (bold && italic) { 312 | font = this.sunwell.options.bodyFontBoldItalic; 313 | } else if (bold) { 314 | font = this.sunwell.options.bodyFontBold; 315 | } else if (italic) { 316 | font = this.sunwell.options.bodyFontItalic; 317 | } else { 318 | font = this.sunwell.options.bodyFontRegular; 319 | } 320 | 321 | return [weight, style, fontSize, `"${font}", sans-serif`].join(" "); 322 | } 323 | 324 | private getLineWidth( 325 | context: CanvasRenderingContext2D, 326 | fontSize: number, 327 | line: string 328 | ): number { 329 | let width = 0; 330 | let bold = 0; 331 | let italic = 0; 332 | 333 | for (const word of line.split(" ")) { 334 | for (const char of chars(word)) { 335 | switch (char) { 336 | case CTRL_MANUAL_LINEBREAKS: 337 | continue; 338 | case CTRL_BOLD_START: 339 | bold += 1; 340 | context.font = this.getFontMaterial(fontSize, !!bold, !!italic); 341 | continue; 342 | case CTRL_BOLD_END: 343 | bold -= 1; 344 | context.font = this.getFontMaterial(fontSize, !!bold, !!italic); 345 | continue; 346 | case CTRL_ITALIC_START: 347 | italic += 1; 348 | context.font = this.getFontMaterial(fontSize, !!bold, !!italic); 349 | continue; 350 | case CTRL_ITALIC_END: 351 | italic -= 1; 352 | context.font = this.getFontMaterial(fontSize, !!bold, !!italic); 353 | continue; 354 | } 355 | 356 | context.fillText( 357 | char, 358 | width + this.sunwell.options.bodyFontOffset.x, 359 | this.sunwell.options.bodyFontOffset.y 360 | ); 361 | 362 | width += context.measureText(char).width; 363 | } 364 | width += 0.275 * context.measureText("M").width; 365 | } 366 | 367 | return width; 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /src/Components/CardArt.ts: -------------------------------------------------------------------------------- 1 | import {IPoint} from "../interfaces"; 2 | import Component from "./Component"; 3 | 4 | const ReferenceWidth = 670; 5 | const ReferenceHeight = 1000; 6 | 7 | /** 8 | * Helper function to draw a polygon from a list of points. 9 | */ 10 | export function drawPolygon( 11 | context: CanvasRenderingContext2D, 12 | points: IPoint[], 13 | ratio: number 14 | ): void { 15 | if (points.length < 3) { 16 | return; 17 | } 18 | context.beginPath(); 19 | // move to start point 20 | context.moveTo(points[0].x * ratio, points[0].y * ratio); 21 | // draw the lines starting at index 1 22 | points.slice(1).forEach(pt => { 23 | context.lineTo(pt.x * ratio, pt.y * ratio); 24 | }); 25 | context.closePath(); 26 | context.stroke(); 27 | } 28 | 29 | export default class CardArt extends Component { 30 | public render(context: CanvasRenderingContext2D, ratio: number): void { 31 | const coords = this.parent.artCoords; 32 | const texture = this.parent.getCardArtTexture(); 33 | 34 | context.save(); 35 | drawPolygon(context, this.parent.artClipPolygon, ratio); 36 | context.clip(); 37 | context.fillStyle = "grey"; 38 | context.fillRect(0, 0, ReferenceWidth * ratio, ReferenceHeight * ratio); 39 | context.drawImage( 40 | texture, 41 | 0, 42 | 0, 43 | texture.width, 44 | texture.height, 45 | coords.dx * ratio, 46 | coords.dy * ratio, 47 | coords.dWidth * ratio, 48 | coords.dHeight * ratio 49 | ); 50 | context.restore(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Components/CardFrame.ts: -------------------------------------------------------------------------------- 1 | import Component from "./Component"; 2 | 3 | export default class CardFrame extends Component { 4 | public assets(): string[] { 5 | return [this.parent.getCardFrameAsset()]; 6 | } 7 | 8 | public render(context: CanvasRenderingContext2D, ratio: number): void { 9 | const coords = this.parent.baseCardFrameCoords; 10 | coords.ratio = ratio; 11 | this.sunwell.drawImage(context, this.parent.getCardFrameAsset(), coords); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Components/Component.ts: -------------------------------------------------------------------------------- 1 | import Card from "../Card"; 2 | import Sunwell from "../Sunwell"; 3 | 4 | export default class Component { 5 | protected sunwell: Sunwell; 6 | protected parent: Card; 7 | 8 | constructor(sunwell: Sunwell, parent: Card) { 9 | this.sunwell = sunwell; 10 | this.parent = parent; 11 | } 12 | 13 | public assets(): string[] { 14 | return []; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Components/CostGem.ts: -------------------------------------------------------------------------------- 1 | import Card from "../Card"; 2 | import Sunwell from "../Sunwell"; 3 | import Gem from "./Gem"; 4 | 5 | export default class CostGem extends Gem { 6 | constructor(sunwell: Sunwell, parent: Card) { 7 | super(sunwell, parent); 8 | this.showGem = true; 9 | this.showText = !parent.cardDef.hideStats; 10 | this.gemAsset = parent.getCostGemAsset(); 11 | this.gemCoords = parent.getCostGemCoords(); 12 | this.text = parent.cardDef.cost.toString(); 13 | this.textColor = parent.costColor; 14 | this.textCoords = parent.getCostTextCoords(); 15 | this.textSize = 130; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Components/EliteDragon.ts: -------------------------------------------------------------------------------- 1 | import Component from "./Component"; 2 | 3 | export default class EliteDragon extends Component { 4 | public assets(): string[] { 5 | return [this.parent.getEliteDragonAsset()]; 6 | } 7 | 8 | public render(context: CanvasRenderingContext2D, ratio: number): void { 9 | const asset = this.parent.getEliteDragonAsset(); 10 | if (!asset) { 11 | return; 12 | } 13 | 14 | const coords = this.parent.eliteDragonCoords; 15 | coords.ratio = ratio; 16 | this.sunwell.drawImage(context, asset, coords); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Components/Gem.ts: -------------------------------------------------------------------------------- 1 | import Card from "../Card"; 2 | import {contextBoundingBox} from "../helpers"; 3 | import {ICoords} from "../interfaces"; 4 | import Sunwell from "../Sunwell"; 5 | import Component from "./Component"; 6 | 7 | export default class Gem extends Component { 8 | public gemAsset: string; 9 | public gemCoords: ICoords; 10 | public showGem: boolean; 11 | public showText: boolean; 12 | public text: string; 13 | public textColor: string; 14 | public textCoords: ICoords; 15 | public textSize: number; 16 | 17 | constructor(sunwell: Sunwell, parent: Card) { 18 | super(sunwell, parent); 19 | this.textSize = 124; 20 | } 21 | 22 | public assets(): string[] { 23 | return [this.gemAsset]; 24 | } 25 | 26 | public render(context: CanvasRenderingContext2D, ratio: number): void { 27 | const asset = this.gemAsset; 28 | if (asset && this.showGem) { 29 | const coords = this.gemCoords; 30 | coords.ratio = ratio; 31 | this.sunwell.drawImage(context, asset, coords); 32 | } 33 | 34 | if (!this.showText || !this.textCoords) { 35 | return; 36 | } 37 | const textCoords = this.textCoords; 38 | textCoords.ratio = ratio; 39 | 40 | const buffer = this.sunwell.getBuffer(256, 256, true); 41 | const bufferCtx = buffer.getContext("2d"); 42 | let tX = 10; 43 | 44 | bufferCtx.font = `${this.textSize}px "${this.sunwell.options.gemFont}"`; 45 | bufferCtx.lineCap = "round"; 46 | bufferCtx.lineJoin = "round"; 47 | bufferCtx.textAlign = "left"; 48 | bufferCtx.textBaseline = "hanging"; 49 | 50 | for (const char of this.text) { 51 | bufferCtx.lineWidth = 10; 52 | bufferCtx.strokeStyle = "black"; 53 | bufferCtx.fillStyle = "black"; 54 | bufferCtx.fillText(char, tX, 10); 55 | bufferCtx.strokeText(char, tX, 10); 56 | 57 | bufferCtx.fillStyle = this.textColor; 58 | bufferCtx.strokeStyle = this.textColor; 59 | bufferCtx.lineWidth = 2.5; 60 | bufferCtx.fillText(char, tX, 10); 61 | // context.strokeText(char, x, y); 62 | 63 | tX += bufferCtx.measureText(char).width; 64 | } 65 | 66 | const b = contextBoundingBox(bufferCtx); 67 | 68 | context.drawImage( 69 | buffer, 70 | b.x, 71 | b.y, 72 | b.w, 73 | b.h, 74 | (textCoords.dx - b.w / 2) * textCoords.ratio, 75 | (textCoords.dy - b.h / 2) * textCoords.ratio, 76 | b.w * textCoords.ratio, 77 | b.h * textCoords.ratio 78 | ); 79 | 80 | this.sunwell.freeBuffer(buffer); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Components/HealthGem.ts: -------------------------------------------------------------------------------- 1 | import Card from "../Card"; 2 | import Sunwell from "../Sunwell"; 3 | import Gem from "./Gem"; 4 | 5 | export default class HealthGem extends Gem { 6 | constructor(sunwell: Sunwell, parent: Card) { 7 | super(sunwell, parent); 8 | this.showGem = !parent.cardDef.hideStats; 9 | this.showText = !parent.cardDef.hideStats; 10 | this.gemAsset = parent.getHealthGemAsset(); 11 | this.gemCoords = parent.getHealthGemCoords(); 12 | this.text = parent.cardDef.health.toString(); 13 | this.textColor = parent.healthColor; 14 | this.textCoords = parent.getHealthTextCoords(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Components/MultiClassBanner.ts: -------------------------------------------------------------------------------- 1 | import {ICoords} from "../interfaces"; 2 | import Component from "./Component"; 3 | 4 | export default class MultiClassBanner extends Component { 5 | public assets(): string[] { 6 | return [this.parent.getMultiClassBannerAsset()]; 7 | } 8 | 9 | public render(context: CanvasRenderingContext2D, ratio: number): void { 10 | const asset = this.parent.getMultiClassBannerAsset(); 11 | if (!asset) { 12 | return; 13 | } 14 | const coords: ICoords = { 15 | dx: 50, 16 | dy: 119, 17 | ratio: ratio, 18 | }; 19 | this.sunwell.drawImage(context, asset, coords); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Components/NameBanner.ts: -------------------------------------------------------------------------------- 1 | import chars from "chars"; 2 | import {getPointOnCurve} from "../helpers"; 3 | import {ICoords, IPoint} from "../interfaces"; 4 | import Component from "./Component"; 5 | 6 | function getCharDimensions(text: string, textContext) { 7 | const dim = []; 8 | const em = textContext.measureText("M").width; 9 | for (const char of chars(text)) { 10 | textContext.save(); 11 | const scale = {x: 1, y: 1}; 12 | let charWidth = textContext.measureText(char).width + 0.1 * em; 13 | switch (char) { 14 | case " ": 15 | charWidth = 0.2 * em; 16 | break; 17 | case "'": // see "Death's Bite" 18 | charWidth = 0.27 * em; 19 | scale.x = 0.5; 20 | scale.y = 1; 21 | break; 22 | } 23 | dim.push({ 24 | scale: scale, 25 | width: charWidth, 26 | }); 27 | textContext.restore(); 28 | } 29 | return dim; 30 | } 31 | export default class NameBanner extends Component { 32 | public assets(): string[] { 33 | return [this.parent.nameBannerAsset]; 34 | } 35 | 36 | public render(context: CanvasRenderingContext2D, ratio: number) { 37 | if (this.parent.nameBannerAsset) { 38 | const coords = this.parent.nameBannerCoords; 39 | coords.ratio = ratio; 40 | this.sunwell.drawImage(context, this.parent.nameBannerAsset, coords); 41 | } 42 | 43 | this.renderName(context, ratio, this.parent.cardDef.name); 44 | } 45 | 46 | private renderName(context: CanvasRenderingContext2D, ratio: number, name: string): void { 47 | // define a box to contain the curved text 48 | const boxDims = {width: 460, height: 160}; 49 | const boxBottomCenter = {x: 335, y: 612}; 50 | // create a new buffer to draw onto 51 | const buffer = this.sunwell.getBuffer(boxDims.width * 2, boxDims.height, true); 52 | const textContext = buffer.getContext("2d"); 53 | const maxWidth = this.parent.nameTextCurve.maxWidth; 54 | const curve = this.parent.nameTextCurve.curve; 55 | textContext.save(); 56 | 57 | textContext.lineCap = "round"; 58 | textContext.lineJoin = "round"; 59 | textContext.lineWidth = 10; 60 | textContext.strokeStyle = "black"; 61 | textContext.textAlign = "left"; 62 | textContext.textBaseline = "middle"; 63 | 64 | let fontSize = 45; 65 | let dimensions = []; 66 | do { 67 | fontSize -= 1; 68 | textContext.font = `${fontSize}px "${this.sunwell.options.titleFont}"`; 69 | } while ( 70 | (dimensions = getCharDimensions(name, textContext)).reduce((a, b) => a + b.width, 0) > 71 | maxWidth && 72 | fontSize > 10 73 | ); 74 | 75 | const textWidth = dimensions.reduce((a, b) => a + b.width, 0) / maxWidth; 76 | const begin = this.parent.nameTextCurve.pathMiddle - textWidth / 2; 77 | const nameChars = chars(name); 78 | const steps = textWidth / nameChars.length; 79 | 80 | // draw text 81 | let p: IPoint; 82 | let t: number; 83 | let leftPos = 0; 84 | 85 | for (let i = 0; i < nameChars.length; i++) { 86 | const char = nameChars[i].trim(); 87 | const dimension = dimensions[i]; 88 | if (leftPos === 0) { 89 | t = begin + steps * i; 90 | p = getPointOnCurve(curve, t); 91 | leftPos = p.x; 92 | } else { 93 | t += 0.01; 94 | p = getPointOnCurve(curve, t); 95 | while (p.x < leftPos) { 96 | t += 0.001; 97 | p = getPointOnCurve(curve, t); 98 | } 99 | } 100 | 101 | if (char.length) { 102 | textContext.save(); 103 | textContext.translate(p.x, p.y); 104 | 105 | if (dimension.scale.x) { 106 | textContext.scale(dimension.scale.x, dimension.scale.y); 107 | } 108 | // textContext.setTransform(1.2, p.r, 0, 1, p.x, p.y); 109 | textContext.rotate(p.r); 110 | 111 | // shadow 112 | textContext.lineWidth = 9 * (fontSize / 50); 113 | textContext.strokeStyle = "black"; 114 | textContext.fillStyle = "black"; 115 | textContext.fillText(char, 0, 0); 116 | textContext.strokeText(char, 0, 0); 117 | 118 | // text 119 | textContext.fillStyle = "white"; 120 | textContext.strokeStyle = "white"; 121 | textContext.lineWidth = 2.5 * (fontSize / 50); 122 | textContext.fillText(char, 0, 0); 123 | 124 | textContext.restore(); 125 | } 126 | 127 | leftPos += dimension.width; 128 | } 129 | 130 | const coords: ICoords = { 131 | sx: 0, 132 | sy: 0, 133 | sWidth: boxDims.width, 134 | sHeight: boxDims.height, 135 | dx: (boxBottomCenter.x - boxDims.width / 2) * ratio, 136 | dy: (boxBottomCenter.y - boxDims.height) * ratio, 137 | dWidth: boxDims.width * ratio, 138 | dHeight: boxDims.height * ratio, 139 | }; 140 | context.drawImage( 141 | buffer, 142 | coords.sx, 143 | coords.sy, 144 | coords.sWidth, 145 | coords.sHeight, 146 | coords.dx, 147 | coords.dy, 148 | coords.dWidth, 149 | coords.dHeight 150 | ); 151 | this.sunwell.freeBuffer(buffer); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Components/RaceBanner.ts: -------------------------------------------------------------------------------- 1 | import chars from "chars"; 2 | import {contextBoundingBox} from "../helpers"; 3 | import Component from "./Component"; 4 | 5 | export default class RaceBanner extends Component { 6 | public assets(): string[] { 7 | return [this.parent.raceBannerAsset]; 8 | } 9 | 10 | public render(context: CanvasRenderingContext2D, ratio: number) { 11 | if (!this.parent.raceBannerAsset || !this.parent.raceText) { 12 | return; 13 | } 14 | const coords = this.parent.raceBannerCoords; 15 | coords.ratio = ratio; 16 | const text = this.parent.raceText; 17 | 18 | // Draw the banner 19 | this.sunwell.drawImage(context, this.parent.raceBannerAsset, coords); 20 | 21 | // Draw the text 22 | const buffer = this.sunwell.getBuffer(300, 60, true); 23 | const bufferCtx = buffer.getContext("2d"); 24 | let x = 10; 25 | const textSize = 40; 26 | 27 | bufferCtx.font = `${textSize}px "${this.sunwell.options.titleFont}"`; 28 | bufferCtx.lineCap = "round"; 29 | bufferCtx.lineJoin = "round"; 30 | bufferCtx.textBaseline = "hanging"; 31 | bufferCtx.textAlign = "left"; 32 | 33 | const xWidth = bufferCtx.measureText("x").width; 34 | for (const char of chars(text)) { 35 | bufferCtx.lineWidth = 7; 36 | bufferCtx.strokeStyle = "black"; 37 | bufferCtx.fillStyle = "black"; 38 | bufferCtx.fillText(char, x, 10); 39 | bufferCtx.strokeText(char, x, 10); 40 | 41 | bufferCtx.fillStyle = "white"; 42 | bufferCtx.strokeStyle = "white"; 43 | bufferCtx.lineWidth = 1; 44 | bufferCtx.fillText(char, x, 10); 45 | // context.strokeText(char, x, y); 46 | 47 | x += bufferCtx.measureText(char).width; 48 | x += xWidth * 0.1; 49 | } 50 | 51 | const b = contextBoundingBox(bufferCtx); 52 | const textCoords = this.parent.raceTextCoords; 53 | 54 | context.drawImage( 55 | buffer, 56 | b.x, 57 | b.y, 58 | b.w, 59 | b.h, 60 | (textCoords.dx - b.w / 2) * ratio, 61 | (textCoords.dy - b.h / 2) * ratio, 62 | b.w * ratio, 63 | b.h * ratio 64 | ); 65 | 66 | this.sunwell.freeBuffer(buffer); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Components/RarityGem.ts: -------------------------------------------------------------------------------- 1 | import Component from "./Component"; 2 | 3 | export default class RarityGem extends Component { 4 | public assets(): string[] { 5 | return [this.parent.getRarityGemAsset()]; 6 | } 7 | 8 | public render(context: CanvasRenderingContext2D, ratio: number): void { 9 | const asset = this.parent.getRarityGemAsset(); 10 | if (!asset) { 11 | return; 12 | } 13 | const coords = this.parent.rarityGemCoords; 14 | coords.ratio = ratio; 15 | this.sunwell.drawImage(context, asset, coords); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Components/Watermark.ts: -------------------------------------------------------------------------------- 1 | import {CardType} from "../Enums"; 2 | import Component from "./Component"; 3 | 4 | export default class Watermark extends Component { 5 | public assets(): string[] { 6 | return [this.parent.getWatermarkAsset()]; 7 | } 8 | 9 | public render(context: CanvasRenderingContext2D, ratio: number): void { 10 | const asset = this.parent.getWatermarkAsset(); 11 | if (!asset) { 12 | return; 13 | } 14 | 15 | if ( 16 | this.parent.premium || 17 | this.parent.cardDef.type === CardType.MINION || 18 | this.parent.cardDef.type === CardType.HERO 19 | ) { 20 | context.globalCompositeOperation = "multiply"; 21 | context.globalAlpha = 0.6; 22 | } else if (this.parent.cardDef.type === CardType.SPELL) { 23 | context.globalCompositeOperation = "multiply"; 24 | context.globalAlpha = 0.7; 25 | } else if (this.parent.cardDef.type === CardType.WEAPON) { 26 | context.globalCompositeOperation = "lighten"; 27 | context.globalAlpha = 0.1; 28 | } 29 | 30 | const coords = this.parent.getWatermarkCoords(); 31 | coords.ratio = ratio; 32 | this.sunwell.drawImage(context, asset, coords); 33 | 34 | // Reset context 35 | context.globalCompositeOperation = "source-over"; 36 | context.globalAlpha = 1; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Enums.ts: -------------------------------------------------------------------------------- 1 | export enum CardClass { 2 | INVALID = 0, 3 | DEATHKNIGHT = 1, 4 | DRUID = 2, 5 | HUNTER = 3, 6 | MAGE = 4, 7 | PALADIN = 5, 8 | PRIEST = 6, 9 | ROGUE = 7, 10 | SHAMAN = 8, 11 | WARLOCK = 9, 12 | WARRIOR = 10, 13 | DREAM = 11, 14 | NEUTRAL = 12, 15 | WHIZBANG = 13, 16 | } 17 | 18 | export enum Race { 19 | INVALID = 0, 20 | MURLOC = 14, 21 | DEMON = 15, 22 | MECHANICAL = 17, 23 | ELEMENTAL = 18, 24 | PET = 20, 25 | BEAST = 20, 26 | TOTEM = 21, 27 | PIRATE = 23, 28 | DRAGON = 24, 29 | ALL = 26, 30 | } 31 | 32 | export enum Rarity { 33 | INVALID = 0, 34 | COMMON = 1, 35 | FREE = 2, 36 | RARE = 3, 37 | EPIC = 4, 38 | LEGENDARY = 5, 39 | } 40 | 41 | export enum MultiClassGroup { 42 | INVALID = 0, 43 | GRIMY_GOONS = 1, 44 | JADE_LOTUS = 2, 45 | KABAL = 3, 46 | } 47 | 48 | export enum CardSet { 49 | INVALID = 0, 50 | CORE = 2, 51 | EXPERT1 = 3, 52 | HOF = 4, 53 | NAXX = 12, 54 | GVG = 13, 55 | BRM = 14, 56 | TGT = 15, 57 | LOE = 20, 58 | KARA = 23, 59 | OG = 21, 60 | GANGS = 25, 61 | UNGORO = 27, 62 | ICECROWN = 1001, 63 | LOOTAPALOOZA = 1004, 64 | GILNEAS = 1125, 65 | BOOMSDAY = 1127, 66 | TROLL = 1129, 67 | DALARAN = 1130, 68 | } 69 | 70 | export enum CardType { 71 | INVALID = 0, 72 | HERO = 3, 73 | MINION = 4, 74 | SPELL = 5, 75 | WEAPON = 7, 76 | HERO_POWER = 10, 77 | } 78 | -------------------------------------------------------------------------------- /src/HeroCard.ts: -------------------------------------------------------------------------------- 1 | import Card from "./Card"; 2 | 3 | export default class HeroCard extends Card { 4 | public premium = false; 5 | public bodyTextColor = "black"; 6 | public bodyTextCoords = { 7 | dx: 143, 8 | dy: 627, 9 | dWidth: 376, 10 | dHeight: 168, 11 | sWidth: 376, 12 | sHeight: 168, 13 | }; 14 | public cardFoundationAsset = null; 15 | public cardFoundationCoords = null; 16 | public baseCardFrameAsset = "frame-hero-"; 17 | public baseCardFrameCoords = { 18 | sWidth: 527, 19 | sHeight: 795, 20 | dx: 70, 21 | dy: 87, 22 | dWidth: 527, 23 | dHeight: 795, 24 | }; 25 | public baseRarityGemAsset = "rarity-"; 26 | public eliteDragonAsset = "elite-hero"; 27 | public eliteDragonCoords = { 28 | dx: 172, 29 | dy: 40, 30 | dWidth: 444, 31 | dHeight: 298, 32 | sWidth: 444, 33 | sHeight: 298, 34 | }; 35 | public nameBannerAsset = "name-banner-hero"; 36 | public nameBannerCoords = { 37 | sWidth: 490, 38 | sHeight: 122, 39 | dx: 91, 40 | dy: 458, 41 | dWidth: 490, 42 | dHeight: 122, 43 | }; 44 | public nameTextCurve = { 45 | pathMiddle: 0.5, 46 | maxWidth: 420, 47 | curve: [{x: 24, y: 98}, {x: 170, y: 36}, {x: 294, y: 36}, {x: 438, y: 96}], 48 | }; 49 | public rarityGemCoords = {dx: 311, dy: 529}; 50 | public artCoords = { 51 | sWidth: 346, 52 | sHeight: 346, 53 | dx: 161, 54 | dy: 137, 55 | dWidth: 346, 56 | dHeight: 346, 57 | }; 58 | public artClipPolygon = [ 59 | {x: 334, y: 134}, 60 | {x: 369, y: 143}, 61 | {x: 406, y: 164}, 62 | {x: 435, y: 187}, 63 | {x: 453, y: 213}, 64 | {x: 469, y: 245}, 65 | {x: 479, y: 270}, 66 | {x: 481, y: 290}, 67 | {x: 483, y: 332}, 68 | {x: 483, y: 380}, 69 | {x: 483, y: 438}, 70 | {x: 484, y: 485}, 71 | {x: 435, y: 473}, 72 | {x: 389, y: 467}, 73 | {x: 346, y: 465}, 74 | {x: 297, y: 466}, 75 | {x: 240, y: 473}, 76 | {x: 185, y: 486}, 77 | {x: 184, y: 445}, 78 | {x: 182, y: 357}, 79 | {x: 184, y: 302}, 80 | {x: 188, y: 271}, 81 | {x: 198, y: 240}, 82 | {x: 210, y: 217}, 83 | {x: 222, y: 198}, 84 | {x: 239, y: 178}, 85 | {x: 262, y: 160}, 86 | {x: 291, y: 145}, 87 | ]; 88 | 89 | public getHealthGemAsset() { 90 | return this.cardDef.armor ? "armor" : "health"; 91 | } 92 | 93 | public getHealthGemCoords() { 94 | if (this.cardDef.armor) { 95 | return { 96 | sWidth: 115, 97 | sHeight: 135, 98 | dx: 498, 99 | dy: 752, 100 | dWidth: 115, 101 | dHeight: 135, 102 | }; 103 | } else { 104 | return { 105 | sWidth: 109, 106 | sHeight: 164, 107 | dx: 504, 108 | dy: 728, 109 | dWidth: 109, 110 | dHeight: 164, 111 | }; 112 | } 113 | } 114 | 115 | public getHealthTextCoords() { 116 | if (this.cardDef.armor) { 117 | return {dx: 554, dy: 822}; 118 | } else { 119 | return {dx: 556, dy: 825}; 120 | } 121 | } 122 | 123 | public getWatermarkCoords() { 124 | return { 125 | dx: 247, 126 | dy: 625, 127 | dWidth: 170, 128 | dHeight: 170, 129 | }; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/HeroCardPremium.ts: -------------------------------------------------------------------------------- 1 | import HeroCard from "./HeroCard"; 2 | 3 | export default class HeroCardPremium extends HeroCard { 4 | public premium = true; 5 | public bodyTextColor = "white"; 6 | public bodyTextCoords = { 7 | dx: 140, 8 | dy: 627, 9 | dWidth: 376, 10 | dHeight: 168, 11 | sWidth: 376, 12 | sHeight: 168, 13 | }; 14 | public cardFoundationAsset = "base-hero-premium"; 15 | public cardFoundationCoords = { 16 | dx: 66, 17 | dy: 84, 18 | dWidth: 527, 19 | dHeight: 799, 20 | sWidth: 527, 21 | sHeight: 799, 22 | }; 23 | public baseCardFrameAsset = "frame-hero-premium-"; 24 | public baseCardFrameCoords = { 25 | dx: 220, 26 | dy: 838, 27 | dWidth: 223, 28 | dHeight: 45, 29 | sWidth: 223, 30 | sHeight: 45, 31 | }; 32 | public eliteDragonAsset = "elite-hero-premium"; 33 | public nameBannerAsset = "name-banner-hero-premium"; 34 | public nameBannerCoords = { 35 | dx: 87, 36 | dy: 456, 37 | dWidth: 490, 38 | dHeight: 122, 39 | sWidth: 490, 40 | sHeight: 122, 41 | }; 42 | public rarityGemCoords = {dx: 307, dy: 528}; 43 | } 44 | -------------------------------------------------------------------------------- /src/HeroPowerCard.ts: -------------------------------------------------------------------------------- 1 | import Card from "./Card"; 2 | 3 | export default class HeroPowerCard extends Card { 4 | public premium = false; 5 | public bodyTextColor = "black"; 6 | public bodyTextCoords = { 7 | dx: 144, 8 | dy: 606, 9 | dWidth: 380, 10 | dHeight: 174, 11 | sWidth: 380, 12 | sHeight: 174, 13 | }; 14 | public cardFoundationAsset = null; 15 | public cardFoundationCoords = null; 16 | public baseCardFrameAsset = "hero-power-"; 17 | public baseCardFrameCoords = { 18 | sWidth: 564, 19 | sHeight: 841, 20 | dx: 56, 21 | dy: 65, 22 | dWidth: 564, 23 | dHeight: 841, 24 | }; 25 | public baseRarityGemAsset = null; 26 | public nameBannerAsset = null; 27 | public nameBannerCoords = null; 28 | public rarityGemCoords = null; 29 | public nameTextCurve = { 30 | pathMiddle: 0.54, 31 | maxWidth: 440, 32 | curve: [{x: 10, y: 37}, {x: 110, y: 37}, {x: 350, y: 37}, {x: 450, y: 37}], 33 | }; 34 | public artCoords = { 35 | sWidth: 261, 36 | sHeight: 261, 37 | dx: 208, 38 | dy: 163, 39 | dWidth: 261, 40 | dHeight: 261, 41 | }; 42 | public artClipPolygon = [ 43 | {x: 344, y: 161}, 44 | {x: 264, y: 173}, 45 | {x: 204, y: 257}, 46 | {x: 207, y: 331}, 47 | {x: 234, y: 394}, 48 | {x: 333, y: 431}, 49 | {x: 424, y: 407}, 50 | {x: 465, y: 355}, 51 | {x: 471, y: 261}, 52 | {x: 427, y: 187}, 53 | ]; 54 | 55 | public getWatermarkCoords() { 56 | return { 57 | dx: 0, 58 | dy: 0, 59 | dWidth: 0, 60 | dHeight: 0, 61 | }; 62 | } 63 | 64 | public getCardFrameAsset(): string { 65 | return this.baseCardFrameAsset + (this.opposing ? "opponent" : "player"); 66 | } 67 | 68 | public getCostGemAsset(): string { 69 | return ""; 70 | } 71 | 72 | public getCostTextCoords() { 73 | return {dx: 338, dy: 124}; 74 | } 75 | 76 | public getRarityGemAsset(): string { 77 | return ""; 78 | } 79 | 80 | public getWatermarkAsset(): string { 81 | return ""; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/HeroPowerCardPremium.ts: -------------------------------------------------------------------------------- 1 | import HeroPowerCard from "./HeroPowerCard"; 2 | 3 | export default class HeroPowerCardPremium extends HeroPowerCard { 4 | public premium = true; 5 | public baseCardFrameAsset = "hero-power-premium-"; 6 | public baseCardFrameCoords = { 7 | dx: 56, 8 | dy: 60, 9 | dWidth: 564, 10 | dHeight: 850, 11 | sWidth: 564, 12 | sHeight: 850, 13 | }; 14 | public costTextCoords = {dx: 338, dy: 130}; 15 | } 16 | -------------------------------------------------------------------------------- /src/MinionCard.ts: -------------------------------------------------------------------------------- 1 | import Card from "./Card"; 2 | import {ICoords} from "./interfaces"; 3 | 4 | export default class MinionCard extends Card { 5 | public premium = false; 6 | public bodyTextColor = "black"; 7 | public bodyTextCoords = { 8 | dx: 130, 9 | dy: 622, 10 | dWidth: 408, 11 | dHeight: 176, 12 | sWidth: 408, 13 | sHeight: 176, 14 | }; 15 | public cardFoundationAsset = null; 16 | public cardFoundationCoords = null; 17 | public baseCardFrameAsset = "frame-minion-"; 18 | public baseCardFrameCoords = { 19 | sWidth: 528, 20 | sHeight: 793, 21 | dx: 70, 22 | dy: 89, 23 | dWidth: 528, 24 | dHeight: 793, 25 | }; 26 | public baseRarityGemAsset = "rarity-minion-"; 27 | public eliteDragonAsset = "elite-minion"; 28 | public eliteDragonCoords = { 29 | dx: 188, 30 | dy: 52, 31 | dWidth: 436, 32 | dHeight: 325, 33 | sWidth: 436, 34 | sHeight: 325, 35 | }; 36 | public nameBannerAsset = "name-banner-minion"; 37 | public raceBannerAsset = "race-banner"; 38 | public rarityGemCoords = {dx: 263, dy: 532}; 39 | public nameBannerCoords = { 40 | sWidth: 485, 41 | sHeight: 113, 42 | dx: 96, 43 | dy: 469, 44 | dWidth: 485, 45 | dHeight: 113, 46 | }; 47 | public nameTextCurve = { 48 | pathMiddle: 0.55, 49 | maxWidth: 450, 50 | curve: [{x: 0, y: 88}, {x: 98, y: 112}, {x: 294, y: 13}, {x: 460, y: 80}], 51 | }; 52 | public artCoords: ICoords = { 53 | sWidth: 461, 54 | sHeight: 461, 55 | dx: 105, 56 | dy: 100, 57 | dWidth: 461, 58 | dHeight: 461, 59 | }; 60 | public artClipPolygon = [ 61 | {x: 335, y: 102}, 62 | {x: 292, y: 110}, 63 | {x: 256, y: 131}, 64 | {x: 222, y: 163}, 65 | {x: 195, y: 203}, 66 | {x: 171, y: 273}, 67 | {x: 163, y: 330}, 68 | {x: 170, y: 398}, 69 | {x: 200, y: 474}, 70 | {x: 266, y: 547}, 71 | {x: 302, y: 563}, 72 | {x: 343, y: 567}, 73 | {x: 406, y: 544}, 74 | {x: 449, y: 506}, 75 | {x: 488, y: 432}, 76 | {x: 505, y: 346}, 77 | {x: 494, y: 255}, 78 | {x: 460, y: 172}, 79 | {x: 425, y: 135}, 80 | {x: 385, y: 111}, 81 | ]; 82 | 83 | public getAttackGemCoords() { 84 | return { 85 | sWidth: 154, 86 | sHeight: 173, 87 | dx: 36, 88 | dy: 721, 89 | dWidth: 154, 90 | dHeight: 173, 91 | }; 92 | } 93 | 94 | public getAttackTextCoords() { 95 | return {dx: 125, dy: 824}; 96 | } 97 | 98 | public getHealthGemCoords() { 99 | return { 100 | sWidth: 109, 101 | sHeight: 164, 102 | dx: 504, 103 | dy: 728, 104 | dWidth: 109, 105 | dHeight: 164, 106 | }; 107 | } 108 | 109 | public getHealthTextCoords() { 110 | return {dx: 556, dy: 825}; 111 | } 112 | 113 | public getAttackGemAsset() { 114 | return "attack-minion"; 115 | } 116 | 117 | public getHealthGemAsset() { 118 | return "health"; 119 | } 120 | 121 | public getWatermarkCoords() { 122 | let dy = 604; 123 | if (this.raceBanner) { 124 | dy -= 10; // Shift up 125 | } 126 | 127 | return { 128 | dx: 231, 129 | dy: dy, 130 | dWidth: 225, 131 | dHeight: 225, 132 | }; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/MinionCardPremium.ts: -------------------------------------------------------------------------------- 1 | import MinionCard from "./MinionCard"; 2 | 3 | export default class MinionCardPremium extends MinionCard { 4 | public premium = true; 5 | public bodyTextColor = "white"; 6 | public cardFoundationAsset = "base-minion-premium"; 7 | public cardFoundationCoords = { 8 | dx: 66, 9 | dy: 64, 10 | dwidth: 528, 11 | dheight: 818, 12 | swidth: 528, 13 | sheight: 818, 14 | }; 15 | public baseCardFrameAsset = "frame-minion-premium-"; 16 | public baseCardFrameCoords = { 17 | dx: 223, 18 | dy: 553, 19 | sWidth: 231, 20 | sHeight: 329, 21 | dWidth: 231, 22 | dHeight: 329, 23 | }; 24 | public baseRarityGemAsset = "rarity-minion-premium-"; 25 | public eliteDragonAsset = "elite-minion-premium"; 26 | public eliteDragonCoords = { 27 | dx: 172, 28 | dy: 17, 29 | sWidth: 485, 30 | sHeight: 341, 31 | dWidth: 485, 32 | dHeight: 341, 33 | }; 34 | public rarityGemCoords = {dx: 245, dy: 528}; 35 | public nameBannerAsset = "name-banner-minion-premium"; 36 | public attackGemAsset = "attack-minion-premium"; 37 | public healthGemAsset = "health-premium"; 38 | public raceBannerAsset = "race-banner-premium"; 39 | public raceBannerCoords = { 40 | dx: 139, 41 | dy: 779, 42 | dWidth: 408, 43 | dHeight: 81, 44 | sWidth: 408, 45 | sHeight: 81, 46 | }; 47 | public raceTextCoords = {dx: 347, dy: 826}; 48 | } 49 | -------------------------------------------------------------------------------- /src/SpellCard.ts: -------------------------------------------------------------------------------- 1 | import Card from "./Card"; 2 | 3 | export default class SpellCard extends Card { 4 | public premium = false; 5 | public bodyTextColor = "black"; 6 | public bodyTextCoords = { 7 | dx: 144, 8 | dy: 630, 9 | dWidth: 378, 10 | dHeight: 168, 11 | sWidth: 378, 12 | sHeight: 168, 13 | }; 14 | public cardFoundationAsset = null; 15 | public cardFoundationCoords = null; 16 | public baseCardFrameAsset = "frame-spell-"; 17 | public baseCardFrameCoords = { 18 | dx: 70, 19 | dy: 133, 20 | dWidth: 527, 21 | dHeight: 746, 22 | }; 23 | public baseRarityGemAsset = "rarity-spell-"; 24 | public eliteDragonAsset = "elite-spell"; 25 | public eliteDragonCoords = { 26 | sWidth: 476, 27 | sHeight: 259, 28 | dx: 185, 29 | dy: 91, 30 | dWidth: 476, 31 | dHeight: 259, 32 | }; 33 | public nameBannerAsset = "name-banner-spell"; 34 | public nameBannerCoords = { 35 | sWidth: 507, 36 | sHeight: 155, 37 | dx: 80, 38 | dy: 457, 39 | dWidth: 507, 40 | dHeight: 155, 41 | }; 42 | public rarityGemCoords = { 43 | sWidth: 116, 44 | sHeight: 77, 45 | dx: 272, 46 | dy: 541, 47 | dWidth: 116, 48 | dHeight: 77, 49 | }; 50 | public nameTextCurve = { 51 | pathMiddle: 0.49, 52 | maxWidth: 450, 53 | curve: [{x: 10, y: 78}, {x: 170, y: 36}, {x: 294, y: 36}, {x: 450, y: 80}], 54 | }; 55 | public artCoords = { 56 | sWidth: 418, 57 | sHeight: 418, 58 | dx: 123, 59 | dy: 138, 60 | dWidth: 418, 61 | dHeight: 418, 62 | }; 63 | public artClipPolygon = [ 64 | {x: 338, y: 171}, 65 | {x: 425, y: 179}, 66 | {x: 544, y: 213}, 67 | {x: 551, y: 474}, 68 | {x: 439, y: 511}, 69 | {x: 327, y: 519}, 70 | {x: 202, y: 505}, 71 | {x: 118, y: 474}, 72 | {x: 116, y: 213}, 73 | {x: 236, y: 176}, 74 | ]; 75 | 76 | public getWatermarkCoords() { 77 | return { 78 | dx: 232, 79 | dy: 612, 80 | dWidth: 210, 81 | dHeight: 210, 82 | }; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/SpellCardPremium.ts: -------------------------------------------------------------------------------- 1 | import SpellCard from "./SpellCard"; 2 | 3 | export default class SpellCardPremium extends SpellCard { 4 | public premium = true; 5 | public bodyTextColor = "white"; 6 | public bodyTextCoords = { 7 | dx: 152, 8 | dy: 634, 9 | dWidth: 366, 10 | dHeight: 168, 11 | sWidth: 366, 12 | sHeight: 168, 13 | }; 14 | public cardFoundationAsset = "base-spell-premium"; 15 | public cardFoundationCoords = { 16 | dx: 52, 17 | dy: 125, 18 | dwidth: 580, 19 | dheight: 755, 20 | swidth: 580, 21 | sheight: 755, 22 | }; 23 | public baseCardFrameAsset = "frame-spell-premium-"; 24 | public baseCardFrameCoords = { 25 | dx: 220, 26 | dy: 126, 27 | sWidth: 226, 28 | sHeight: 754, 29 | dWidth: 226, 30 | dHeight: 754, 31 | }; 32 | public baseRarityGemAsset = "rarity-spell-premium-"; 33 | public eliteDragonAsset = "elite-spell-premium"; 34 | public eliteDragonCoords = { 35 | dx: 185, 36 | dy: 91, 37 | dWidth: 476, 38 | dHeight: 259, 39 | sWidth: 476, 40 | sHeight: 259, 41 | }; 42 | public nameBannerAsset = "name-banner-spell-premium"; 43 | public nameBannerCoords = { 44 | dx: 84, 45 | dy: 464, 46 | dWidth: 497, 47 | dHeight: 152, 48 | sWidth: 497, 49 | sHeight: 152, 50 | }; 51 | public nameTextCurve = { 52 | pathMiddle: 0.49, 53 | maxWidth: 450, 54 | curve: [{x: 10, y: 86}, {x: 170, y: 44}, {x: 294, y: 44}, {x: 450, y: 88}], 55 | }; 56 | public rarityGemCoords = { 57 | dx: 283, 58 | dy: 545, 59 | dWidth: 107, 60 | dHeight: 74, 61 | sWidth: 107, 62 | sHeight: 74, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/Sunwell.ts: -------------------------------------------------------------------------------- 1 | /*#if _PLATFORM == "node" 2 | import Platform from "./platforms/NodePlatform.ts"; 3 | //#else */ 4 | import Platform from "./platforms/WebPlatform"; 5 | //#endif 6 | 7 | import Card from "./Card"; 8 | import {CardType} from "./Enums"; 9 | import {cleanEnum} from "./helpers"; 10 | import HeroCard from "./HeroCard"; 11 | import HeroCardPremium from "./HeroCardPremium"; 12 | import HeroPowerCard from "./HeroPowerCard"; 13 | import HeroPowerCardPremium from "./HeroPowerCardPremium"; 14 | import {ICoords} from "./interfaces"; 15 | import MinionCard from "./MinionCard"; 16 | import MinionCardPremium from "./MinionCardPremium"; 17 | import SpellCard from "./SpellCard"; 18 | import SpellCardPremium from "./SpellCardPremium"; 19 | import WeaponCard from "./WeaponCard"; 20 | import WeaponCardPremium from "./WeaponCardPremium"; 21 | 22 | interface ISunwellOptions { 23 | titleFont: string; 24 | bodyFontRegular: string; 25 | bodyFontBold: string; 26 | bodyFontItalic: string; 27 | bodyFontBoldItalic: string; 28 | gemFont: string; 29 | aspectRatio: number; 30 | bodyFontSize: number; 31 | bodyFontOffset: {x: number; y: number}; 32 | bodyLineHeight: number; 33 | bodyLineStyle: string; 34 | assetFolder: string; 35 | drawTimeout: number; 36 | cacheSkeleton: boolean; 37 | debug: boolean; 38 | } 39 | 40 | export default class Sunwell { 41 | public options: ISunwellOptions; 42 | public assets: {[key: string]: HTMLImageElement}; 43 | public canvas: HTMLCanvasElement; 44 | public target: any; 45 | public platform: Platform; 46 | public renderCache: {[cacheKey: string]: any}; 47 | 48 | private assetListeners: { 49 | [path: string]: Array<(HTMLCanvasElement) => void>; 50 | }; 51 | private renderQuery: {[key: string]: Card}; 52 | private isRendering: boolean; 53 | 54 | constructor(options: ISunwellOptions) { 55 | options.titleFont = options.titleFont || "Belwe"; 56 | options.bodyFontRegular = options.bodyFontRegular || "Franklin Gothic"; 57 | options.bodyFontBold = options.bodyFontBold || options.bodyFontRegular; 58 | options.bodyFontItalic = options.bodyFontItalic || options.bodyFontRegular; 59 | options.bodyFontBoldItalic = options.bodyFontBoldItalic || options.bodyFontRegular; 60 | options.gemFont = options.gemFont || "Belwe"; 61 | options.aspectRatio = options.aspectRatio || 1.492537; 62 | options.bodyFontSize = options.bodyFontSize || 60; 63 | options.bodyFontOffset = options.bodyFontOffset || {x: 0, y: 0}; 64 | options.bodyLineHeight = options.bodyLineHeight || 50; 65 | options.bodyLineStyle = options.bodyLineStyle || "1em"; 66 | options.assetFolder = options.assetFolder || "/assets/"; 67 | options.drawTimeout = options.drawTimeout || 5000; 68 | options.cacheSkeleton = options.cacheSkeleton || false; 69 | options.debug = options.debug || false; 70 | 71 | this.platform = new Platform(); 72 | this.options = options; 73 | this.assets = {}; 74 | this.assetListeners = {}; 75 | this.renderQuery = {}; 76 | this.renderCache = {}; 77 | this.isRendering = false; 78 | } 79 | 80 | public log(...args: any[]): void { 81 | if (this.options.debug) { 82 | console.log.apply("[INFO]", arguments); 83 | } 84 | } 85 | 86 | public error(...args: any[]): void { 87 | console.log.apply("[ERROR]", arguments); 88 | } 89 | 90 | public drawImage(context: CanvasRenderingContext2D, assetKey: string, coords: ICoords): void { 91 | const asset = this.getAsset(assetKey); 92 | if (!asset) { 93 | this.error("Not drawing asset", assetKey); 94 | return; 95 | } 96 | const ratio = coords.ratio || 1; 97 | const width = coords.sWidth || asset.width; 98 | const height = coords.sHeight || asset.height; 99 | context.drawImage( 100 | asset, 101 | coords.sx || 0, 102 | coords.sy || 0, 103 | width, 104 | height, 105 | coords.dx * ratio, 106 | coords.dy * ratio, 107 | (coords.dWidth || width) * ratio, 108 | (coords.dHeight || height) * ratio 109 | ); 110 | } 111 | 112 | public fetchAsset(path: string) { 113 | const assets = this.assets; 114 | const assetListeners = this.assetListeners; 115 | const sw = this; 116 | 117 | return new this.platform.Promise(resolve => { 118 | if (assets[path] === undefined) { 119 | assets[path] = new sw.platform.Image(); 120 | 121 | sw.log("Requesting", path); 122 | sw.platform.loadAsset( 123 | assets[path], 124 | path, 125 | () => { 126 | if (assetListeners[path]) { 127 | for (const listener of assetListeners[path]) { 128 | listener(assets[path]); 129 | } 130 | delete assetListeners[path]; 131 | } 132 | resolve(); 133 | }, 134 | () => { 135 | sw.error("Error loading asset:", path); 136 | // An asset load error should not reject the promise 137 | resolve(); 138 | } 139 | ); 140 | } else if (!assets[path].complete) { 141 | assetListeners[path] = assetListeners[path] || []; 142 | assetListeners[path].push(resolve); 143 | } else { 144 | resolve(); 145 | } 146 | }); 147 | } 148 | 149 | public getBuffer(width?: number, height?: number, clear?: boolean): HTMLCanvasElement { 150 | return this.platform.getBuffer(width, height, clear); 151 | } 152 | 153 | public freeBuffer(buffer: HTMLCanvasElement) { 154 | return this.platform.freeBuffer(buffer); 155 | } 156 | 157 | public render(): void { 158 | const keys = Object.keys(this.renderQuery); 159 | if (!keys.length) { 160 | return; 161 | } 162 | 163 | const first = keys[0]; 164 | const card = this.renderQuery[first]; 165 | delete this.renderQuery[first]; 166 | 167 | const context = card.canvas.getContext("2d"); 168 | 169 | this.log("Preparing assets for", card.cardDef.name); 170 | 171 | const texturesToLoad: string[] = []; 172 | 173 | if (card.texture && typeof card.texture === "string") { 174 | texturesToLoad.push(card.texture); 175 | } 176 | 177 | for (const asset of card.getAssetsToLoad()) { 178 | if (!asset) { 179 | continue; 180 | } 181 | const path = this.getAssetPath(asset); 182 | if (!this.assets[path] || !this.assets[path].complete) { 183 | texturesToLoad.push(path); 184 | } 185 | } 186 | 187 | this.log("Preparing to load assets"); 188 | const fetches: Array> = []; 189 | for (const texture of texturesToLoad) { 190 | fetches.push(this.fetchAsset(texture)); 191 | } 192 | 193 | this.platform.Promise.all(fetches) 194 | .then(() => { 195 | const start = Date.now(); 196 | card.draw(card.canvas, context); 197 | this.log(card, "finished drawing in " + (Date.now() - start) + "ms"); 198 | // check whether we have more to do 199 | this.isRendering = false; 200 | if (Object.keys(this.renderQuery).length) { 201 | this.renderTick(); 202 | } 203 | }) 204 | .catch(e => { 205 | this.error("Error while drawing card:", e); 206 | this.isRendering = false; 207 | }); 208 | } 209 | 210 | public getAssetPath(key: string): string { 211 | return this.options.assetFolder + key + ".png"; 212 | } 213 | 214 | public getAsset(key: string) { 215 | const path = this.getAssetPath(key); 216 | const asset = this.assets[path]; 217 | if (!asset) { 218 | this.error("Missing asset", key, "at", path); 219 | return; 220 | } 221 | if (!asset.complete) { 222 | this.error("Attempting to getAsset not loaded", asset, path); 223 | return; 224 | } 225 | return asset; 226 | } 227 | 228 | public renderTick(): void { 229 | this.isRendering = true; 230 | this.platform.requestAnimationFrame(() => this.render()); 231 | } 232 | 233 | public createCard( 234 | props, 235 | width: number, 236 | premium: boolean, 237 | target, 238 | callback?: (HTMLCanvasElement) => void 239 | ): Card { 240 | let canvas: HTMLCanvasElement; 241 | const height = Math.round(width * this.options.aspectRatio); 242 | 243 | if (target && target instanceof HTMLCanvasElement) { 244 | canvas = target; 245 | canvas.width = width; 246 | canvas.height = height; 247 | } else { 248 | canvas = this.getBuffer(width, height, true); 249 | } 250 | 251 | const ctors: {[type: number]: any} = {}; 252 | ctors[CardType.HERO] = premium ? HeroCardPremium : HeroCard; 253 | ctors[CardType.MINION] = premium ? MinionCardPremium : MinionCard; 254 | ctors[CardType.SPELL] = premium ? SpellCardPremium : SpellCard; 255 | ctors[CardType.WEAPON] = premium ? WeaponCardPremium : WeaponCard; 256 | ctors[CardType.HERO_POWER] = premium ? HeroPowerCardPremium : HeroPowerCard; 257 | 258 | const type = cleanEnum(props.type, CardType); 259 | 260 | const ctor = ctors[type]; 261 | if (!ctor) { 262 | throw new Error(`Got an unrenderable card type: ${type}`); 263 | } 264 | 265 | const card: Card = new ctor(this, props); 266 | card.canvas = canvas; 267 | card.initRender(width, target, callback); 268 | 269 | this.log("Queried render:", card.cardDef.name); 270 | if (this.renderQuery[card.key]) { 271 | this.log("Skipping", card.key, "(already queued)"); 272 | } else { 273 | this.renderQuery[card.key] = card; 274 | if (!this.isRendering) { 275 | this.renderTick(); 276 | } 277 | } 278 | 279 | return card; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/WeaponCard.ts: -------------------------------------------------------------------------------- 1 | import Card from "./Card"; 2 | 3 | export default class WeaponCard extends Card { 4 | public premium = false; 5 | public bodyTextColor = "white"; 6 | public bodyTextCoords = { 7 | dx: 146, 8 | dy: 628, 9 | dWidth: 388, 10 | dHeight: 168, 11 | sWidth: 388, 12 | sHeight: 168, 13 | }; 14 | public cardFoundationAsset = null; 15 | public cardFoundationCoords = null; 16 | public baseCardFrameAsset = "frame-weapon-"; 17 | public baseCardFrameCoords = { 18 | sWidth: 527, 19 | sHeight: 775, 20 | dx: 80, 21 | dy: 103, 22 | dWidth: 527, 23 | dHeight: 775, 24 | }; 25 | public baseRarityGemAsset = "rarity-weapon-"; 26 | public eliteDragonAsset = "elite-weapon"; 27 | public eliteDragonCoords = { 28 | dx: 199, 29 | dy: 62, 30 | dWidth: 420, 31 | dHeight: 247, 32 | sWidth: 420, 33 | sHeight: 247, 34 | }; 35 | public nameBannerAsset = "name-banner-weapon"; 36 | public nameBannerCoords = { 37 | sWidth: 514, 38 | sHeight: 108, 39 | dx: 87, 40 | dy: 468, 41 | dWidth: 514, 42 | dHeight: 108, 43 | }; 44 | public rarityGemCoords = { 45 | sWidth: 96, 46 | sHeight: 90, 47 | dx: 302, 48 | dy: 520, 49 | dWidth: 96, 50 | dHeight: 90, 51 | }; 52 | public nameTextCurve = { 53 | pathMiddle: 0.56, 54 | maxWidth: 450, 55 | curve: [{x: 18, y: 56}, {x: 66, y: 56}, {x: 400, y: 56}, {x: 456, y: 56}], 56 | }; 57 | public artCoords = { 58 | sWidth: 384, 59 | sHeight: 384, 60 | dx: 152, 61 | dy: 135, 62 | dWidth: 384, 63 | dHeight: 384, 64 | }; 65 | public artClipPolygon = [ 66 | {x: 352, y: 139}, 67 | {x: 418, y: 155}, 68 | {x: 469, y: 188}, 69 | {x: 497, y: 222}, 70 | {x: 523, y: 267}, 71 | {x: 533, y: 315}, 72 | {x: 531, y: 366}, 73 | {x: 514, y: 420}, 74 | {x: 485, y: 461}, 75 | {x: 444, y: 496}, 76 | {x: 375, y: 515}, 77 | {x: 309, y: 515}, 78 | {x: 236, y: 484}, 79 | {x: 192, y: 434}, 80 | {x: 160, y: 371}, 81 | {x: 158, y: 303}, 82 | {x: 173, y: 246}, 83 | {x: 203, y: 201}, 84 | {x: 242, y: 167}, 85 | {x: 287, y: 148}, 86 | ]; 87 | 88 | public getAttackGemAsset() { 89 | return "attack-weapon"; 90 | } 91 | 92 | public getAttackGemCoords() { 93 | return { 94 | sWidth: 135, 95 | sHeight: 133, 96 | dx: 65, 97 | dy: 753, 98 | dWidth: 135, 99 | dHeight: 133, 100 | }; 101 | } 102 | 103 | public getAttackTextCoords() { 104 | return {dx: 136, dy: 820}; 105 | } 106 | 107 | public getHealthGemAsset() { 108 | return "durability"; 109 | } 110 | 111 | public getHealthGemCoords() { 112 | return { 113 | sWidth: 126, 114 | sHeight: 140, 115 | dx: 501, 116 | dy: 744, 117 | dWidth: 126, 118 | dHeight: 140, 119 | }; 120 | } 121 | 122 | public getHealthTextCoords() { 123 | return {dx: 563, dy: 819}; 124 | } 125 | 126 | public getWatermarkCoords() { 127 | return { 128 | dx: 241, 129 | dy: 599, 130 | dWidth: 220, 131 | dHeight: 220, 132 | }; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/WeaponCardPremium.ts: -------------------------------------------------------------------------------- 1 | import WeaponCard from "./WeaponCard"; 2 | 3 | export default class WeaponCardPremium extends WeaponCard { 4 | public premium = true; 5 | public attackGemAsset = "attack-weapon-premium"; 6 | public cardFoundationAsset = "base-weapon-premium"; 7 | public cardFoundationCoords = { 8 | dx: 75, 9 | dy: 101, 10 | dWidth: 527, 11 | dHeight: 778, 12 | sWidth: 527, 13 | sHeight: 778, 14 | }; 15 | public baseCardFrameAsset = "frame-weapon-premium-"; 16 | public baseCardFrameCoords = { 17 | dx: 229, 18 | dy: 532, 19 | dWidth: 226, 20 | dHeight: 347, 21 | sWidth: 226, 22 | sHeight: 347, 23 | }; 24 | public eliteDragonAsset = "elite-weapon-premium"; 25 | public eliteDragonCoords = { 26 | dx: 197, 27 | dy: 63, 28 | dWidth: 420, 29 | dHeight: 247, 30 | sWidth: 420, 31 | sHeight: 247, 32 | }; 33 | public healthGemAsset = "durability-premium"; 34 | public nameBannerAsset = "name-banner-weapon-premium"; 35 | } 36 | -------------------------------------------------------------------------------- /src/helpers.ts: -------------------------------------------------------------------------------- 1 | import {CardClass, CardSet, CardType, Race, Rarity} from "./Enums"; 2 | import {IPoint} from "./interfaces"; 3 | 4 | const RaceNames = { 5 | [Race.MURLOC]: { 6 | enUS: "Murloc", 7 | frFR: "Murloc", 8 | deDE: "Murloc", 9 | koKR: "멀록", 10 | esES: "Múrloc", 11 | esMX: "Múrloc", 12 | ruRU: "Мурлок", 13 | zhTW: "魚人", 14 | zhCN: "鱼人", 15 | itIT: "Murloc", 16 | ptBR: "Murloc", 17 | plPL: "Murlok", 18 | jaJP: "マーロック", 19 | thTH: "เมอร์ล็อค", 20 | }, 21 | [Race.MECHANICAL]: { 22 | enUS: "Mech", 23 | frFR: "Méca", 24 | deDE: "Mech", 25 | koKR: "기계", 26 | esES: "Robot", 27 | esMX: "Meca", 28 | ruRU: "Механизм", 29 | zhTW: "機械", 30 | zhCN: "机械", 31 | itIT: "Robot", 32 | ptBR: "Mecanoide", 33 | plPL: "Mech", 34 | jaJP: "メカ", 35 | thTH: "เครื่องจักร", 36 | }, 37 | [Race.ELEMENTAL]: { 38 | enUS: "Elemental", 39 | frFR: "Élémentaire", 40 | deDE: "Elementar", 41 | koKR: "정령", 42 | esES: "Elemental", 43 | esMX: "Elemental", 44 | ruRU: "Элементаль", 45 | zhTW: "元素", 46 | zhCN: "元素", 47 | itIT: "Elementale", 48 | ptBR: "Elemental", 49 | plPL: "Żywiołak", 50 | jaJP: "エレメンタル", 51 | thTH: "วิญญาณธาตุ", 52 | }, 53 | [Race.PET]: { 54 | enUS: "Beast", 55 | frFR: "Bête", 56 | deDE: "Wildtier", 57 | koKR: "야수", 58 | esES: "Bestia", 59 | esMX: "Bestia", 60 | ruRU: "Зверь", 61 | zhTW: "野獸", 62 | zhCN: "野兽", 63 | itIT: "Bestia", 64 | ptBR: "Fera", 65 | plPL: "Bestia", 66 | jaJP: "獣", 67 | thTH: "สัตว์", 68 | }, 69 | [Race.DEMON]: { 70 | enUS: "Demon", 71 | frFR: "Démon", 72 | deDE: "Dämon", 73 | koKR: "악마", 74 | esES: "Demonio", 75 | esMX: "Demonio", 76 | ruRU: "Демон", 77 | zhTW: "惡魔", 78 | zhCN: "恶魔", 79 | itIT: "Demone", 80 | ptBR: "Demônio", 81 | plPL: "Demon", 82 | jaJP: "悪魔", 83 | thTH: "ปีศาจ", 84 | }, 85 | [Race.PIRATE]: { 86 | enUS: "Pirate", 87 | frFR: "Pirate", 88 | deDE: "Pirat", 89 | koKR: "해적", 90 | esES: "Pirata", 91 | esMX: "Pirata", 92 | ruRU: "Пират", 93 | zhTW: "海盜", 94 | zhCN: "海盗", 95 | itIT: "Pirata", 96 | ptBR: "Pirata", 97 | plPL: "Pirat", 98 | jaJP: "海賊", 99 | thTH: "โจรสลัด", 100 | }, 101 | [Race.DRAGON]: { 102 | enUS: "Dragon", 103 | frFR: "Dragon", 104 | deDE: "Drache", 105 | koKR: "용족", 106 | esES: "Dragón", 107 | esMX: "Dragón", 108 | ruRU: "Дракон", 109 | zhTW: "龍類", 110 | zhCN: "龙", 111 | itIT: "Drago", 112 | ptBR: "Dragão", 113 | plPL: "Smok", 114 | jaJP: "ドラゴン", 115 | thTH: "มังกร", 116 | }, 117 | [Race.TOTEM]: { 118 | enUS: "Totem", 119 | frFR: "Totem", 120 | deDE: "Totem", 121 | koKR: "토템", 122 | esES: "Tótem", 123 | esMX: "Tótem", 124 | ruRU: "Тотем", 125 | zhTW: "圖騰", 126 | zhCN: "图腾", 127 | itIT: "Totem", 128 | ptBR: "Totem", 129 | plPL: "Totem", 130 | jaJP: "トーテム", 131 | thTH: "โทเท็ม", 132 | }, 133 | [Race.ALL]: { 134 | enUS: "All", 135 | frFR: "Tout", 136 | deDE: "Alle", 137 | koKR: "모두", 138 | esES: "Todos", 139 | esMX: "Todas", 140 | ruRU: "Все", 141 | zhTW: "全部", 142 | zhCN: "全部", 143 | itIT: "Tutti", 144 | ptBR: "Tudo", 145 | plPL: "Wszystkie", 146 | jaJP: "全て", 147 | thTH: "ทุกอย่าง", 148 | }, 149 | }; 150 | 151 | export function cleanEnum(val: string | number, e) { 152 | if (typeof val === "string") { 153 | if (val in e) { 154 | return e[val]; 155 | } else { 156 | return e.INVALID; 157 | } 158 | } 159 | return val || 0; 160 | } 161 | 162 | /** 163 | * Get the bounding box of a canvas content. 164 | * @returns {{x: *, y: *, maxX: (number|*|w), maxY: *, w: number, h: number}} 165 | */ 166 | export function contextBoundingBox(context: CanvasRenderingContext2D) { 167 | const w = context.canvas.width; 168 | const h = context.canvas.height; 169 | const data = context.getImageData(0, 0, w, h).data; 170 | let minX = 999; 171 | let minY = 999; 172 | let maxX = 0; 173 | let maxY = 0; 174 | 175 | let out = false; 176 | 177 | for (let y = h - 1; y > -1; y--) { 178 | if (out) { 179 | break; 180 | } 181 | for (let x = 0; x < w; x++) { 182 | if (data[y * (w * 4) + x * 4 + 3] > 0) { 183 | maxY = Math.max(maxY, y); 184 | out = true; 185 | break; 186 | } 187 | } 188 | } 189 | 190 | if (maxY === undefined) { 191 | return null; 192 | } 193 | 194 | out2: for (let x = w - 1; x > -1; x--) { 195 | for (let y = 0; y < h; y++) { 196 | if (data[y * (w * 4) + x * 4 + 3] > 0) { 197 | maxX = Math.max(maxX, x); 198 | break out2; 199 | } 200 | } 201 | } 202 | 203 | out3: for (let x = 0; x < maxX; x++) { 204 | for (let y = 0; y < h; y++) { 205 | if (data[y * (w * 4) + x * 4 + 3] > 0) { 206 | minX = Math.min(x, minX); 207 | break out3; 208 | } 209 | } 210 | } 211 | 212 | out4: for (let y = 0; y < maxY; y++) { 213 | for (let x = 0; x < w; x++) { 214 | if (data[y * (w * 4) + x * 4 + 3] > 0) { 215 | minY = Math.min(minY, y); 216 | break out4; 217 | } 218 | } 219 | } 220 | 221 | return { 222 | x: minX, 223 | y: minY, 224 | maxX: maxX, 225 | maxY: maxY, 226 | w: maxX - minX, 227 | h: maxY - minY, 228 | }; 229 | } 230 | 231 | export function getNumberStyle(style: string) { 232 | switch (style) { 233 | case "-": 234 | return "#f00"; 235 | case "+": 236 | return "#0f0"; 237 | default: 238 | return "white"; 239 | } 240 | } 241 | 242 | /** 243 | * Given a curve and t, the function returns the point on the curve. 244 | * r is the rotation of the point in radians. 245 | * @returns {{x: (number|*), y: (number|*), r: number}} 246 | */ 247 | export function getPointOnCurve(curve: IPoint[], t: number): IPoint { 248 | const rX = 249 | 3 * Math.pow(1 - t, 2) * (curve[1].x - curve[0].x) + 250 | 6 * (1 - t) * t * (curve[2].x - curve[1].x) + 251 | 3 * Math.pow(t, 2) * (curve[3].x - curve[2].x); 252 | const rY = 253 | 3 * Math.pow(1 - t, 2) * (curve[1].y - curve[0].y) + 254 | 6 * (1 - t) * t * (curve[2].y - curve[1].y) + 255 | 3 * Math.pow(t, 2) * (curve[3].y - curve[2].y); 256 | 257 | const x = 258 | Math.pow(1 - t, 3) * curve[0].x + 259 | 3 * Math.pow(1 - t, 2) * t * curve[1].x + 260 | 3 * (1 - t) * Math.pow(t, 2) * curve[2].x + 261 | Math.pow(t, 3) * curve[3].x; 262 | const y = 263 | Math.pow(1 - t, 3) * curve[0].y + 264 | 3 * Math.pow(1 - t, 2) * t * curve[1].y + 265 | 3 * (1 - t) * Math.pow(t, 2) * curve[2].y + 266 | Math.pow(t, 3) * curve[3].y; 267 | 268 | return {x: x, y: y, r: Math.atan2(rY, rX)}; 269 | } 270 | 271 | export function getCardFrameClass(cardClass: CardClass): CardClass { 272 | switch (cardClass) { 273 | case CardClass.DREAM: 274 | return CardClass.HUNTER; 275 | case CardClass.INVALID: 276 | case CardClass.WHIZBANG: 277 | return CardClass.NEUTRAL; 278 | default: 279 | return cardClass; 280 | } 281 | } 282 | 283 | export function getRaceText(race: Race, cardType: CardType, language: string): string { 284 | if (cardType === CardType.MINION && race in RaceNames) { 285 | return RaceNames[race][language] || ""; 286 | } 287 | return ""; 288 | } 289 | 290 | export function getRarityGem(rarity: Rarity, set: CardSet, type?: CardType): Rarity { 291 | switch (rarity) { 292 | case Rarity.INVALID: 293 | case Rarity.FREE: 294 | return type === CardType.HERO ? Rarity.COMMON : null; 295 | case Rarity.COMMON: 296 | if (set === CardSet.CORE) { 297 | return null; 298 | } 299 | } 300 | return rarity; 301 | } 302 | -------------------------------------------------------------------------------- /src/interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface ICoords { 2 | sx?: number; 3 | sy?: number; 4 | sWidth?: number; 5 | sHeight?: number; 6 | dx: number; 7 | dy: number; 8 | dWidth?: number; 9 | dHeight?: number; 10 | ratio?: number; 11 | } 12 | 13 | export interface IPoint { 14 | x: number; 15 | y: number; 16 | r?: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/platforms/IPlatform.ts: -------------------------------------------------------------------------------- 1 | interface IPlatform { 2 | name: string; 3 | buffers: any[]; 4 | Image: any; 5 | Promise: any; 6 | getBuffer(width: number, height: number, clear: boolean): void; 7 | freeBuffer(buffer): void; 8 | loadAsset(img, url, loaded, error): void; 9 | requestAnimationFrame(cb: () => void): void; 10 | } 11 | 12 | export default IPlatform; 13 | 14 | export interface IPlatformConstructable { 15 | new (): IPlatform; 16 | } 17 | -------------------------------------------------------------------------------- /src/platforms/NodePlatform.ts: -------------------------------------------------------------------------------- 1 | import * as Canvas from "canvas"; 2 | import * as fs from "fs"; 3 | 4 | import IPlatform from "./IPlatform"; 5 | 6 | export default class NodePlatform implements IPlatform { 7 | public name = "NODE"; 8 | public buffers = []; 9 | public Image = Canvas.Image; 10 | public Promise = Promise; 11 | 12 | public getBuffer(width: number, height: number, clear: boolean): void { 13 | return Canvas.createCanvas(width, height); 14 | } 15 | 16 | public freeBuffer(buffer: any): void { 17 | // Nothing to do 18 | } 19 | 20 | public loadAsset(img: any, path: any, loaded: any, error: any): void { 21 | fs.readFile(path, (err, data) => { 22 | if (err) { 23 | error(); 24 | return; 25 | } 26 | img.src = data; 27 | loaded(); 28 | }); 29 | } 30 | 31 | public requestAnimationFrame(cb) { 32 | setImmediate(cb); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/platforms/WebPlatform.ts: -------------------------------------------------------------------------------- 1 | import IPlatform from "./IPlatform"; 2 | 3 | export default class WebPlatform implements IPlatform { 4 | public name = "WEB"; 5 | public Image = Image; 6 | public Promise = Promise; 7 | public buffers = []; 8 | 9 | public getBuffer(width?: number, height?: number, clear?: boolean): HTMLCanvasElement { 10 | let cvs: HTMLCanvasElement; 11 | if (this.buffers.length) { 12 | if (width) { 13 | for (let i = 0; i < this.buffers.length; i++) { 14 | if (this.buffers[i].width === width && this.buffers[i].height === height) { 15 | cvs = this.buffers.splice(i, 1)[0]; 16 | break; 17 | } 18 | } 19 | } else { 20 | cvs = this.buffers.pop(); 21 | } 22 | if (cvs) { 23 | if (clear) { 24 | cvs.getContext("2d").clearRect(0, 0, cvs.width, cvs.height); 25 | } 26 | return cvs; 27 | } 28 | } 29 | 30 | cvs = document.createElement("canvas"); 31 | 32 | if (width) { 33 | cvs.width = width; 34 | cvs.height = height; 35 | } 36 | 37 | return cvs; 38 | } 39 | 40 | public freeBuffer(buffer: any): void { 41 | this.buffers.push(buffer); 42 | } 43 | 44 | public loadAsset(img: any, url: any, loaded: any, error: any): void { 45 | img.crossOrigin = "Anonymous"; 46 | img.addEventListener("load", loaded); 47 | img.addEventListener("error", error); 48 | img.src = url; 49 | } 50 | 51 | public requestAnimationFrame(cb) { 52 | window.requestAnimationFrame(cb); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Document 13 | 14 | 15 | 16 | 17 | 18 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": false, 4 | "lib": ["es6", "dom"], 5 | "target": "es5", 6 | "outDir": "dist/", 7 | "moduleResolution": "node", 8 | "isolatedModules": false, 9 | "removeComments": false, 10 | "noLib": false, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": true 13 | }, 14 | "include": ["./src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:recommended"], 4 | "jsRules": { 5 | "indent": [false] 6 | }, 7 | "rules": { 8 | "arrow-parens": [false], 9 | "comment-format": [false], 10 | "indent": [false], 11 | "object-literal-shorthand": [false], 12 | "object-literal-sort-keys": [false], 13 | "no-bitwise": [false], 14 | "no-conditional-assignment": [false], 15 | "trailing-comma": [false] 16 | }, 17 | "rulesDirectory": [] 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const nodeExternals = require("webpack-node-externals"); 4 | 5 | const PROD = process.env.NODE_ENV === "production"; 6 | const PLATFORM = process.env.PLATFORM || "node"; 7 | 8 | const TS_LOADER = {test: /\.ts$/, loaders: ["ts-loader"]}; 9 | 10 | let plugins = [ 11 | new webpack.DefinePlugin({ 12 | "process.env.PLATFORM": JSON.stringify(PLATFORM), 13 | }), 14 | ]; 15 | 16 | // if (PROD) { 17 | plugins.push( 18 | new webpack.optimize.UglifyJsPlugin({ 19 | compress: true, 20 | mangle: false, 21 | beautify: true, 22 | }) 23 | ); 24 | // plugins.push(new webpack.optimize.ModuleConcatenationPlugin()) 25 | // } 26 | 27 | module.exports = [ 28 | { 29 | name: "sunwell", 30 | target: PLATFORM, 31 | entry: { 32 | sunwell: path.join(__dirname, "src/Sunwell.ts"), 33 | }, 34 | output: { 35 | path: path.resolve(__dirname, "dist"), 36 | filename: PROD ? "[name].min.js" : "[name].js", 37 | library: "Sunwell", 38 | // libraryTarget: PLATFORM == 'web' ? 'var' : 'commonjs2' 39 | libraryTarget: "umd", 40 | umdNamedDefine: true, 41 | }, 42 | resolve: { 43 | extensions: [".webpack.js", ".web.js", ".ts", ".js"], 44 | }, 45 | externals: [nodeExternals()], 46 | module: { 47 | loaders: [TS_LOADER], 48 | }, 49 | plugins, 50 | }, 51 | ]; 52 | --------------------------------------------------------------------------------