From 61e9e9bb3aa10dec6bf815d7fe84b244df952f68 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 01/46] Convert project to TypeScript --- .eslintrc.json | 28 +- package-lock.json | 974 ++++++++++++++++++ package.json | 5 +- rollup.config.js | 28 +- src/{Color.js => Color.ts} | 32 +- src/{Costume.js => Costume.ts} | 8 +- src/{Input.js => Input.ts} | 34 +- src/{Loudness.js => Loudness.ts} | 39 +- src/{Project.js => Project.ts} | 96 +- src/{Renderer.js => Renderer.ts} | 245 +++-- src/{Sound.js => Sound.ts} | 444 ++++---- src/{Sprite.js => Sprite.ts} | 383 ++++--- src/{Trigger.js => Trigger.ts} | 49 +- src/{Watcher.js => Watcher.ts} | 85 +- src/{index.js => index.ts} | 0 ...e-adpcm-audio.js => decode-adpcm-audio.ts} | 23 +- src/renderer/{BitmapSkin.js => BitmapSkin.ts} | 8 +- src/renderer/{Drawable.js => Drawable.ts} | 89 +- src/renderer/{Matrix.js => Matrix.ts} | 16 +- src/renderer/{PenSkin.js => PenSkin.ts} | 20 +- src/renderer/{Rectangle.js => Rectangle.ts} | 36 +- src/renderer/ShaderManager.js | 135 --- src/renderer/ShaderManager.ts | 157 +++ src/renderer/{Shaders.js => Shaders.ts} | 21 +- src/renderer/{Skin.js => Skin.ts} | 35 +- ...peechBubbleSkin.js => SpeechBubbleSkin.ts} | 44 +- src/renderer/{VectorSkin.js => VectorSkin.ts} | 47 +- src/renderer/{effectInfo.js => effectInfo.ts} | 17 +- ...nsformPoint.js => effectTransformPoint.ts} | 7 +- tsconfig.json | 19 + 30 files changed, 2384 insertions(+), 740 deletions(-) rename src/{Color.js => Color.ts} (85%) rename src/{Costume.js => Costume.ts} (71%) rename src/{Input.js => Input.ts} (73%) rename src/{Loudness.js => Loudness.ts} (65%) rename src/{Project.js => Project.ts} (73%) rename src/{Renderer.js => Renderer.ts} (78%) rename src/{Sound.js => Sound.ts} (69%) rename src/{Sprite.js => Sprite.ts} (62%) rename src/{Trigger.js => Trigger.ts} (63%) rename src/{Watcher.js => Watcher.ts} (72%) rename src/{index.js => index.ts} (100%) rename src/lib/{decode-adpcm-audio.js => decode-adpcm-audio.ts} (88%) rename src/renderer/{BitmapSkin.js => BitmapSkin.ts} (84%) rename src/renderer/{Drawable.js => Drawable.ts} (81%) rename src/renderer/{Matrix.js => Matrix.ts} (83%) rename src/renderer/{PenSkin.js => PenSkin.ts} (87%) rename src/renderer/{Rectangle.js => Rectangle.ts} (80%) delete mode 100644 src/renderer/ShaderManager.js create mode 100644 src/renderer/ShaderManager.ts rename src/renderer/{Shaders.js => Shaders.ts} (96%) rename src/renderer/{Skin.js => Skin.ts} (67%) rename src/renderer/{SpeechBubbleSkin.js => SpeechBubbleSkin.ts} (80%) rename src/renderer/{VectorSkin.js => VectorSkin.ts} (73%) rename src/renderer/{effectInfo.js => effectInfo.ts} (61%) rename src/renderer/{effectTransformPoint.js => effectTransformPoint.ts} (94%) create mode 100644 tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index b99e945..a256112 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,13 +3,35 @@ "ecmaVersion": 9, "sourceType": "module" }, - "plugins": ["prettier"], - "extends": ["eslint:recommended", "plugin:prettier/recommended"], + "plugins": ["@typescript-eslint", "prettier"], + "extends": [ + "eslint:recommended", + "plugin:prettier/recommended" + ], "env": { "browser": true, "es6": true }, "rules": { "no-console": ["warn", {"allow": ["warn", "error"]}] - } + }, + "overrides": [ + { + "files": ["src/**/*"], + "parserOptions": { + "parser": "@typescript-eslint/parser", + "project": "./tsconfig.json" + }, + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking" + ], + "env": { + "browser": true + }, + "rules": { + "@typescript-eslint/no-non-null-assertion": "off" + } + } + ] } diff --git a/package-lock.json b/package-lock.json index ad939f1..6b5a4a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "1.5.1", "license": "MIT", "devDependencies": { + "@rollup/plugin-typescript": "^8.3.4", + "@typescript-eslint/eslint-plugin": "^5.31.0", + "@typescript-eslint/parser": "^5.31.0", "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", @@ -169,6 +172,87 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/plugin-typescript": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.4.tgz", + "integrity": "sha512-wt7JnYE9antX6BOXtsxGoeVSu4dZfw0dU3xykfOQ4hC3EddxRbVG/K0xiY1Wup7QOHJcjLYXWAn0Kx9Z1SBHHg==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "rollup": "^2.14.0", + "tslib": "*", + "typescript": ">=3.7.0" + }, + "peerDependenciesMeta": { + "tslib": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -184,12 +268,230 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "node_modules/@types/node": { "version": "18.0.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.5.tgz", "integrity": "sha512-En7tneq+j0qAiVwysBD79y86MT3ModuoIJbe7JXp+sb5UAjInSShmK3nXXMioBzfF7rXC12hv12d4IyCVwN4dA==", "dev": true }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz", + "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.31.0", + "@typescript-eslint/type-utils": "5.31.0", + "@typescript-eslint/utils": "5.31.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.31.0.tgz", + "integrity": "sha512-UStjQiZ9OFTFReTrN+iGrC6O/ko9LVDhreEK5S3edmXgR396JGq7CoX2TWIptqt/ESzU2iRKXAHfSF2WJFcWHw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.31.0", + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/typescript-estree": "5.31.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz", + "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/visitor-keys": "5.31.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz", + "integrity": "sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.31.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz", + "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz", + "integrity": "sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/visitor-keys": "5.31.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.31.0.tgz", + "integrity": "sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.31.0", + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/typescript-estree": "5.31.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz", + "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.31.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/acorn": { "version": "8.7.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", @@ -248,6 +550,15 @@ "node": ">=4" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -270,6 +581,18 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz", @@ -590,6 +913,18 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -1004,6 +1339,34 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -1016,6 +1379,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -1028,6 +1400,18 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -1135,6 +1519,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -1287,6 +1691,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1401,6 +1814,18 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "dev": true }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -1413,6 +1838,28 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -1579,12 +2026,33 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", @@ -2207,6 +2675,26 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -2254,6 +2742,16 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2406,6 +2904,29 @@ "estree-walker": "^0.6.1" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2432,6 +2953,21 @@ "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", "dev": true }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -2462,6 +2998,15 @@ "node": ">=8" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2625,6 +3170,39 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2649,6 +3227,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", @@ -2726,6 +3318,12 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", @@ -2863,6 +3461,61 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@rollup/plugin-typescript": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.3.4.tgz", + "integrity": "sha512-wt7JnYE9antX6BOXtsxGoeVSu4dZfw0dU3xykfOQ4hC3EddxRbVG/K0xiY1Wup7QOHJcjLYXWAn0Kx9Z1SBHHg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^3.1.0", + "resolve": "^1.17.0" + } + }, + "@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "dependencies": { + "estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true + } + } + }, "@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -2875,12 +3528,137 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, "@types/node": { "version": "18.0.5", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.5.tgz", "integrity": "sha512-En7tneq+j0qAiVwysBD79y86MT3ModuoIJbe7JXp+sb5UAjInSShmK3nXXMioBzfF7rXC12hv12d4IyCVwN4dA==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.31.0.tgz", + "integrity": "sha512-VKW4JPHzG5yhYQrQ1AzXgVgX8ZAJEvCz0QI6mLRX4tf7rnFfh5D8SKm0Pq6w5PyNfAWJk6sv313+nEt3ohWMBQ==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.31.0", + "@typescript-eslint/type-utils": "5.31.0", + "@typescript-eslint/utils": "5.31.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.31.0.tgz", + "integrity": "sha512-UStjQiZ9OFTFReTrN+iGrC6O/ko9LVDhreEK5S3edmXgR396JGq7CoX2TWIptqt/ESzU2iRKXAHfSF2WJFcWHw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.31.0", + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/typescript-estree": "5.31.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.31.0.tgz", + "integrity": "sha512-8jfEzBYDBG88rcXFxajdVavGxb5/XKXyvWgvD8Qix3EEJLCFIdVloJw+r9ww0wbyNLOTYyBsR+4ALNGdlalLLg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/visitor-keys": "5.31.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.31.0.tgz", + "integrity": "sha512-7ZYqFbvEvYXFn9ax02GsPcEOmuWNg+14HIf4q+oUuLnMbpJ6eHAivCg7tZMVwzrIuzX3QCeAOqKoyMZCv5xe+w==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.31.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.31.0.tgz", + "integrity": "sha512-/f/rMaEseux+I4wmR6mfpM2wvtNZb1p9hAV77hWfuKc3pmaANp5dLAZSiE3/8oXTYTt3uV9KW5yZKJsMievp6g==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.31.0.tgz", + "integrity": "sha512-3S625TMcARX71wBc2qubHaoUwMEn+l9TCsaIzYI/ET31Xm2c9YQ+zhGgpydjorwQO9pLfR/6peTzS/0G3J/hDw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/visitor-keys": "5.31.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.31.0.tgz", + "integrity": "sha512-kcVPdQS6VIpVTQ7QnGNKMFtdJdvnStkqS5LeALr4rcwx11G6OWb2HB17NMPnlRHvaZP38hL9iK8DdE9Fne7NYg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.31.0", + "@typescript-eslint/types": "5.31.0", + "@typescript-eslint/typescript-estree": "5.31.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "dependencies": { + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.31.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.31.0.tgz", + "integrity": "sha512-ZK0jVxSjS4gnPirpVjXHz7mgdOsZUHzNYSfTw2yPa3agfbt9YfqaBiBZFSSxeBWnpWkzCxTfUpnzA3Vily/CSg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.31.0", + "eslint-visitor-keys": "^3.3.0" + } + }, "acorn": { "version": "8.7.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", @@ -2921,6 +3699,12 @@ "color-convert": "^1.9.0" } }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2943,6 +3727,15 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "browserslist": { "version": "4.21.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.2.tgz", @@ -3170,6 +3963,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3469,6 +4271,30 @@ "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", "dev": true }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3481,6 +4307,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3490,6 +4325,15 @@ "flat-cache": "^3.0.4" } }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -3572,6 +4416,20 @@ "type-fest": "^0.20.2" } }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, "has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -3688,6 +4546,12 @@ "is-extglob": "^2.1.1" } }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3786,6 +4650,15 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "dev": true }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, "mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -3798,6 +4671,22 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3922,12 +4811,24 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, "pify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", @@ -4308,6 +5209,12 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -4340,6 +5247,12 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -4452,6 +5365,15 @@ "estree-walker": "^0.6.1" } }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4464,6 +5386,15 @@ "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==", "dev": true }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, "serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -4488,6 +5419,12 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4610,6 +5547,30 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4625,6 +5586,13 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "peer": true + }, "update-browserslist-db": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz", @@ -4677,6 +5645,12 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/package.json b/package.json index 9c8acd1..2753b71 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "dev": "rollup -c --watch", "build": "rollup -c", "prepare": "npm run build", - "lint": "eslint \"./src/**.js\"" + "lint": "eslint \"./src/**.ts\"" }, "files": [ "dist/" @@ -30,6 +30,9 @@ }, "homepage": "https://leopardjs.com/", "devDependencies": { + "@rollup/plugin-typescript": "^8.3.4", + "@typescript-eslint/eslint-plugin": "^5.31.0", + "@typescript-eslint/parser": "^5.31.0", "eslint": "^8.20.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", diff --git a/rollup.config.js b/rollup.config.js index a76f3d6..c488056 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,23 +1,25 @@ import postcss from "rollup-plugin-postcss"; import { terser } from "rollup-plugin-terser"; +import typescript from "@rollup/plugin-typescript"; export default [ { - input: "src/index.js", + input: "src/index.ts", output: [ { file: "dist/index.esm.js", format: "esm", - sourcemap: true + sourcemap: true, }, { file: "dist/index.umd.js", format: "umd", name: "leopard", - sourcemap: true - } + sourcemap: true, + }, ], plugins: [ + typescript(), terser({ output: { comments: (node, comment) => { @@ -26,22 +28,22 @@ export default [ return /license/i.test(comment.value); } return false; - } - } - }) - ] + }, + }, + }), + ], }, { input: "src/index.css", output: { - file: "dist/index.min.css" + file: "dist/index.min.css", }, plugins: [ postcss({ modules: false, extract: true, - minimize: true - }) - ] - } + minimize: true, + }), + ], + }, ]; diff --git a/src/Color.js b/src/Color.ts similarity index 85% rename from src/Color.js rename to src/Color.ts index 674c8ea..b724d0d 100644 --- a/src/Color.js +++ b/src/Color.ts @@ -1,7 +1,8 @@ -const clamp = (n, min, max) => Math.max(min, Math.min(max, n)); +const clamp = (n: number, min: number, max: number) => + Math.max(min, Math.min(max, n)); // https://www.rapidtables.com/convert/color/rgb-to-hsv.html -function rgbToHSV(r, g, b) { +function rgbToHSV(r: number, g: number, b: number) { r /= 255; g /= 255; b /= 255; @@ -26,17 +27,17 @@ function rgbToHSV(r, g, b) { s = delta / max; } - let v = max; + const v = max; return { h: h * 100, s: s * 100, - v: v * 100 + v: v * 100, }; } // https://www.rapidtables.com/convert/color/hsv-to-rgb.html -function hsvToRGB(h, s, v) { +function hsvToRGB(h: number, s: number, v: number) { h = (h / 100) * 360; s /= 100; v /= 100; @@ -73,11 +74,16 @@ function hsvToRGB(h, s, v) { return { r: r * 255, g: g * 255, - b: b * 255 + b: b * 255, }; } export default class Color { + _h = 0; + _s = 0; + _v = 0; + _a = 1; + constructor(h = 0, s = 0, v = 0, a = 1) { this.h = h; this.s = s; @@ -85,16 +91,16 @@ export default class Color { this.a = a; } - static rgb(r, g, b, a = 1) { + static rgb(r: number, g: number, b: number, a = 1) { const { h, s, v } = rgbToHSV(r, g, b); return new Color(h, s, v, a); } - static hsv(h, s, v, a = 1) { + static hsv(h: number, s: number, v: number, a = 1) { return new Color(h, s, v, a); } - static num(n) { + static num(n: number | string) { n = Number(n); // Match Scratch rgba system @@ -162,7 +168,7 @@ export default class Color { this._v = clamp(v, 0, 100); } - _setRGB(r, g, b) { + _setRGB(r: number, g: number, b: number) { r = clamp(r, 0, 255); g = clamp(g, 0, 255); b = clamp(b, 0, 255); @@ -175,7 +181,7 @@ export default class Color { } toHexString(forceIncludeAlpha = false) { - const toHexDigits = n => { + const toHexDigits = (n: number) => { n = clamp(Math.round(n), 0, 255); let str = n.toString(16); @@ -203,12 +209,12 @@ export default class Color { return `rgb(${rgb.join(", ")})`; } - toRGBA() { + toRGBA(): [number, number, number, number] { const rgb = hsvToRGB(this._h, this._s, this._v); return [rgb.r, rgb.g, rgb.b, this._a * 255]; } - toRGBANormalized() { + toRGBANormalized(): [number, number, number, number] { const rgb = hsvToRGB(this._h, this._s, this._v); return [rgb.r / 255, rgb.g / 255, rgb.b / 255, this._a]; } diff --git a/src/Costume.js b/src/Costume.ts similarity index 71% rename from src/Costume.js rename to src/Costume.ts index 5a0bd60..d5f087d 100644 --- a/src/Costume.js +++ b/src/Costume.ts @@ -1,5 +1,11 @@ export default class Costume { - constructor(name, url, center = { x: 0, y: 0 }) { + name: string; + url: string; + img: HTMLImageElement; + isBitmap: boolean; + resolution: 2 | 1; + center: { x: number; y: number }; + constructor(name: string, url: string, center = { x: 0, y: 0 }) { this.name = name; this.url = url; diff --git a/src/Input.js b/src/Input.ts similarity index 73% rename from src/Input.js rename to src/Input.ts index 4f9f207..9c455a6 100644 --- a/src/Input.js +++ b/src/Input.ts @@ -1,5 +1,17 @@ +import type { Stage } from "./Sprite.js"; + export default class Input { - constructor(stage, canvas, onKeyDown) { + _stage: Stage; + _canvas: HTMLCanvasElement; + _onKeyDown: (key: string) => unknown; + + mouse: { x: number; y: number; down: boolean }; + keys: string[]; + constructor( + stage: Input["_stage"], + canvas: Input["_canvas"], + onKeyDown: Input["_onKeyDown"] + ) { this._stage = stage; this._canvas = canvas; @@ -20,42 +32,42 @@ export default class Input { this._onKeyDown = onKeyDown; } - _mouseMove(e) { + _mouseMove(e: MouseEvent) { const rect = this._canvas.getBoundingClientRect(); const scaleX = this._stage.width / rect.width; const scaleY = this._stage.height / rect.height; const realCoords = { x: (e.clientX - rect.left) * scaleX, - y: (e.clientY - rect.top) * scaleY + y: (e.clientY - rect.top) * scaleY, }; this.mouse = { ...this.mouse, x: realCoords.x - this._stage.width / 2, - y: -realCoords.y + this._stage.height / 2 + y: -realCoords.y + this._stage.height / 2, }; } _mouseDown() { this.mouse = { ...this.mouse, - down: true + down: true, }; } _mouseUp() { this.mouse = { ...this.mouse, - down: false + down: false, }; } - _keyup(e) { + _keyup(e: KeyboardEvent) { const key = this._getKeyName(e); - this.keys = this.keys.filter(k => k !== key); + this.keys = this.keys.filter((k) => k !== key); } - _keydown(e) { + _keydown(e: KeyboardEvent) { e.preventDefault(); const key = this._getKeyName(e); @@ -66,7 +78,7 @@ export default class Input { this._onKeyDown(key); } - _getKeyName(e) { + _getKeyName(e: KeyboardEvent) { if (e.key === "ArrowUp") return "up arrow"; if (e.key === "ArrowDown") return "down arrow"; if (e.key === "ArrowLeft") return "left arrow"; @@ -77,7 +89,7 @@ export default class Input { return e.key.toLowerCase(); } - keyPressed(name) { + keyPressed(name: string) { if (name === "any") return this.keys.length > 0; return this.keys.indexOf(name) > -1; } diff --git a/src/Loudness.js b/src/Loudness.ts similarity index 65% rename from src/Loudness.js rename to src/Loudness.ts index c2aaa7f..2e215c9 100644 --- a/src/Loudness.js +++ b/src/Loudness.ts @@ -2,11 +2,28 @@ import Sound from "./Sound.js"; const IGNORABLE_ERROR = ["NotAllowedError", "NotFoundError"]; +const enum ConnectionState { + /** We have not tried connecting yet. */ + NOT_CONNECTED, + /** We are in the middle of connecting. */ + CONNECTING, + /** We connected successfully. */ + CONNECTED, + /** There was an error connecting. */ + ERROR, +} + // https://github.com/LLK/scratch-audio/blob/develop/src/Loudness.js export default class LoudnessHandler { + connectionState: ConnectionState; + audioStream: MediaStream | undefined; + analyser: AnalyserNode | undefined; + micDataArray: Float32Array | undefined; + _lastValue: number | undefined; + constructor() { // TODO: use a TypeScript enum - this.connectionState = "NOT_CONNECTED"; + this.connectionState = ConnectionState.NOT_CONNECTED; } get audioContext() { @@ -16,24 +33,23 @@ export default class LoudnessHandler { async connect() { // If we're in the middle of connecting, or failed to connect, // don't attempt to connect again - if (this.connectionState !== "NOT_CONNECTED") return; - this.connectionState = "CONNECTING"; + if (this.connectionState !== ConnectionState.NOT_CONNECTED) return; + this.connectionState = ConnectionState.CONNECTING; try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); // Chrome blocks usage of audio until the user interacts with the page. // By calling `resume` here, we will wait until that happens. await Sound.audioContext.resume(); - this.hasConnected = true; this.audioStream = stream; const mic = this.audioContext.createMediaStreamSource(stream); this.analyser = this.audioContext.createAnalyser(); mic.connect(this.analyser); this.micDataArray = new Float32Array(this.analyser.fftSize); - this.connectionState = "CONNECTED"; + this.connectionState = ConnectionState.CONNECTED; } catch (e) { - this.connectionState = "ERROR"; - if (IGNORABLE_ERROR.includes(e.name)) { + this.connectionState = ConnectionState.ERROR; + if (IGNORABLE_ERROR.includes((e as Error).name)) { console.warn("Mic is not available."); } else { throw e; @@ -42,7 +58,12 @@ export default class LoudnessHandler { } get loudness() { - if (this.connectionState !== "CONNECTED" || !this.audioStream.active) { + if ( + this.connectionState !== ConnectionState.CONNECTED || + !this.audioStream?.active || + !this.analyser || + !this.micDataArray + ) { return -1; } @@ -68,7 +89,7 @@ export default class LoudnessHandler { } getLoudness() { - this.connect(); + void this.connect(); return this.loudness; } } diff --git a/src/Project.js b/src/Project.ts similarity index 73% rename from src/Project.js rename to src/Project.ts index 66d2456..1b6d2f2 100644 --- a/src/Project.js +++ b/src/Project.ts @@ -1,11 +1,30 @@ -import Trigger from "./Trigger.js"; +import Trigger, { TriggerOptions } from "./Trigger.js"; import Renderer from "./Renderer.js"; import Input from "./Input.js"; import LoudnessHandler from "./Loudness.js"; import Sound from "./Sound.js"; +import type { Stage, Sprite } from "./Sprite.js"; + +type RunningTrigger = { + target: Sprite | Stage; + trigger: Trigger; +}; export default class Project { - constructor(stage, sprites = {}, { frameRate = 30 } = {}) { + stage: Stage; + sprites: Partial>; + renderer: Renderer; + input: Input; + + loudnessHandler: LoudnessHandler; + _cachedLoudness: number | null; + + runningTriggers: RunningTrigger[]; + _prevStepTriggerPredicates: WeakMap; + answer: string | null; + timerStart!: Date; + + constructor(stage: Stage, sprites = {}, { frameRate = 30 } = {}) { this.stage = stage; this.sprites = sprites; @@ -16,9 +35,9 @@ export default class Project { } this.stage._project = this; - this.renderer = new Renderer(this); - this.input = new Input(this.stage, this.renderer.stage, key => { - this.fireTrigger(Trigger.KEY_PRESSED, { key }); + this.renderer = new Renderer(this, null); + this.input = new Input(this.stage, this.renderer.stage, (key) => { + void this.fireTrigger(Trigger.KEY_PRESSED, { key }); }); this.loudnessHandler = new LoudnessHandler(); @@ -43,7 +62,7 @@ export default class Project { this._renderLoop(); } - attach(renderTarget) { + attach(renderTarget: string | HTMLElement) { this.renderer.setRenderTarget(renderTarget); this.renderer.stage.addEventListener("click", () => { // Chrome requires a user gesture on the page before we can start the @@ -51,12 +70,12 @@ export default class Project { // When we click the stage, that counts as a user gesture, so try // resuming the audio context. if (Sound.audioContext.state === "suspended") { - Sound.audioContext.resume(); + void Sound.audioContext.resume(); } let clickedSprite = this.renderer.pick(this.spritesAndClones, { x: this.input.mouse.x, - y: this.input.mouse.y + y: this.input.mouse.y, }); if (!clickedSprite) { clickedSprite = this.stage; @@ -69,7 +88,7 @@ export default class Project { } } - this._startTriggers(matchingTriggers); + void this._startTriggers(matchingTriggers); }); } @@ -79,18 +98,20 @@ export default class Project { // When greenFlag is triggered, it's likely that the cause of it was some // kind of button click, so try resuming the audio context. if (Sound.audioContext.state === "suspended") { - Sound.audioContext.resume(); + void Sound.audioContext.resume(); } - this.fireTrigger(Trigger.GREEN_FLAG); + void this.fireTrigger(Trigger.GREEN_FLAG); this.input.focus(); } // Find triggers which match the given condition - _matchingTriggers(triggerMatches) { - let matchingTriggers = []; + _matchingTriggers( + triggerMatches: (tr: Trigger, target: Sprite | Stage) => boolean + ) { + const matchingTriggers = []; const targets = this.spritesAndStage; for (const target of targets) { - const matchingTargetTriggers = target.triggers.filter(tr => + const matchingTargetTriggers = target.triggers.filter((tr) => triggerMatches(tr, target) ); for (const match of matchingTargetTriggers) { @@ -101,25 +122,26 @@ export default class Project { } _stepEdgeActivatedTriggers() { - const edgeActivated = this._matchingTriggers(tr => tr.isEdgeActivated); + const edgeActivated = this._matchingTriggers((tr) => tr.isEdgeActivated); const triggersToStart = []; for (const triggerWithTarget of edgeActivated) { const { trigger, target } = triggerWithTarget; let predicate; switch (trigger.trigger) { case Trigger.TIMER_GREATER_THAN: - predicate = this.timer > trigger.option("VALUE", target); + predicate = this.timer > trigger.option("VALUE", target)!; break; case Trigger.LOUDNESS_GREATER_THAN: - predicate = this.loudness > trigger.option("VALUE", target); + predicate = this.loudness > trigger.option("VALUE", target)!; break; default: - throw new Error(`Unimplemented trigger ${trigger.trigger}`); + throw new Error(`Unimplemented trigger ${String(trigger.trigger)}`); } // Default to false - const prevPredicate = !!this._prevStepTriggerPredicates.get(trigger); - this._prevStepTriggerPredicates.set(trigger, predicate); + const prevPredicate = + !!this._prevStepTriggerPredicates.get(triggerWithTarget); + this._prevStepTriggerPredicates.set(triggerWithTarget, predicate); // The predicate evaluated to false last time and true this time // Activate the trigger @@ -127,7 +149,7 @@ export default class Project { triggersToStart.push(triggerWithTarget); } } - this._startTriggers(triggersToStart); + void this._startTriggers(triggersToStart); } step() { @@ -148,12 +170,14 @@ export default class Project { render() { // Render to canvas - this.renderer.update(this.stage, this.spritesAndClones); + this.renderer.update(); // Update watchers - for (const sprite of [...Object.values(this.sprites), this.stage]) { - for (const watcher of Object.values(sprite.watchers)) { - watcher.updateDOM(this.renderer.renderTarget); + if (this.renderer.renderTarget) { + for (const sprite of [...Object.values(this.sprites), this.stage]) { + for (const watcher of Object.values(sprite!.watchers)) { + watcher!.updateDOM(this.renderer.renderTarget); + } } } } @@ -163,7 +187,7 @@ export default class Project { this.render(); } - fireTrigger(trigger, options) { + fireTrigger(trigger: symbol, options?: TriggerOptions) { // Special trigger behaviors if (trigger === Trigger.GREEN_FLAG) { this.restartTimer(); @@ -171,7 +195,7 @@ export default class Project { this.runningTriggers = []; for (const spriteName in this.sprites) { - const sprite = this.sprites[spriteName]; + const sprite = this.sprites[spriteName]!; sprite.clones = []; } @@ -188,14 +212,14 @@ export default class Project { return this._startTriggers(matchingTriggers); } - _startTriggers(triggers) { + _startTriggers(triggers: RunningTrigger[]) { // Only add these triggers to this.runningTriggers if they're not already there. // TODO: if the triggers are already running, they'll be restarted but their execution order is unchanged. // Does that match Scratch's behavior? for (const trigger of triggers) { if ( !this.runningTriggers.find( - runningTrigger => + (runningTrigger) => trigger.trigger === runningTrigger.trigger && trigger.target === runningTrigger.target ) @@ -212,7 +236,7 @@ export default class Project { get spritesAndClones() { return Object.values(this.sprites) - .flatMap(sprite => sprite.andClones()) + .flatMap((sprite) => sprite!.andClones()) .sort((a, b) => a._layerOrder - b._layerOrder); } @@ -220,8 +244,12 @@ export default class Project { return [...this.spritesAndClones, this.stage]; } - changeSpriteLayer(sprite, layerDelta, relativeToSprite = sprite) { - let spritesArray = this.spritesAndClones; + changeSpriteLayer( + sprite: Sprite, + layerDelta: number, + relativeToSprite = sprite + ) { + const spritesArray = this.spritesAndClones; const originalIndex = spritesArray.indexOf(sprite); const relativeToIndex = spritesArray.indexOf(relativeToSprite); @@ -249,7 +277,7 @@ export default class Project { } get timer() { - const ms = new Date() - this.timerStart; + const ms = new Date().getTime() - this.timerStart.getTime(); return ms / 1000; } @@ -257,7 +285,7 @@ export default class Project { this.timerStart = new Date(); } - async askAndWait(question) { + async askAndWait(question: string) { this.answer = await this.renderer.displayAskBox(question); } diff --git a/src/Renderer.js b/src/Renderer.ts similarity index 78% rename from src/Renderer.js rename to src/Renderer.ts index 3b8cede..a0ce7b2 100644 --- a/src/Renderer.js +++ b/src/Renderer.ts @@ -1,14 +1,18 @@ -import Matrix from "./renderer/Matrix.js"; +import Matrix, { MatrixType } from "./renderer/Matrix.js"; import Drawable from "./renderer/Drawable.js"; import BitmapSkin from "./renderer/BitmapSkin.js"; import PenSkin from "./renderer/PenSkin.js"; import SpeechBubbleSkin from "./renderer/SpeechBubbleSkin.js"; import VectorSkin from "./renderer/VectorSkin.js"; import Rectangle from "./renderer/Rectangle.js"; -import ShaderManager from "./renderer/ShaderManager.js"; +import ShaderManager, { Shader, DrawMode } from "./renderer/ShaderManager.js"; import { effectNames, effectBitmasks } from "./renderer/effectInfo.js"; +import type Skin from "./renderer/Skin.js"; import Costume from "./Costume.js"; +import type Color from "./Color.js"; +import type Project from "./Project.js"; +import { Sprite, Stage, _EffectMap, SpeechBubble } from "./Sprite.js"; // Rectangle used for checking collision bounds. // Rather than create a new one each time, we can just reuse this one. @@ -18,23 +22,56 @@ const __collisionBox = new Rectangle(); // stored in the blue channel, then green, then red. // RGB [0, 0, 0] is reserved for "no sprite here". // This allows for up to 2^24 - 2 different sprites to be rendered at once. -const idToColor = id => [ +const idToColor = (id: number): [number, number, number] => [ (((id + 1) >> 16) & 0xff) / 255, (((id + 1) >> 8) & 0xff) / 255, - ((id + 1) & 0xff) / 255 + ((id + 1) & 0xff) / 255, ]; // Convert a 24-bit color back into a sprite ID/index number. // -1 means "no sprite here". -const colorToId = ([r, g, b]) => ((r << 16) | (g << 8) | b) - 1; +const colorToId = ([r, g, b]: [number, number, number] | Uint8Array): number => + ((r << 16) | (g << 8) | b) - 1; + +type RenderSpriteOptions = { + drawMode: DrawMode; + effectMask?: number; + colorMask?: [number, number, number, number]; + renderSpeechBubbles?: boolean; + spriteColorId?: (target: Sprite | Stage) => number; +}; + +export type FramebufferInfo = { + texture: WebGLTexture; + width: number; + height: number; + framebuffer: WebGLFramebuffer; +}; export default class Renderer { - constructor(project, renderTarget) { + project: Project; + stage: HTMLCanvasElement; + gl: WebGLRenderingContext; + renderTarget: HTMLElement | null = null; + + _shaderManager: ShaderManager; + _drawables: WeakMap; + _skins: WeakMap; + + _currentShader: Shader | null; + _currentFramebuffer: WebGLFramebuffer | null; + _screenSpaceScale: number; + _penSkin: PenSkin; + _collisionBuffer: FramebufferInfo; + + constructor(project: Project, renderTarget: HTMLElement | string | null) { const w = project.stage.width; const h = project.stage.height; this.project = project; this.stage = this.createStage(w, h); - this.gl = this.stage.getContext("webgl", { antialias: false }); + const gl = this.stage.getContext("webgl", { antialias: false }); + if (gl === null) throw new Error("Could not initialize WebGL context"); + this.gl = gl; if (renderTarget) { this.setRenderTarget(renderTarget); @@ -51,7 +88,6 @@ export default class Renderer { this._screenSpaceScale = 1; // Initialize a bunch of WebGL state - const gl = this.gl; // Use premultiplied alpha for proper color blending. gl.enable(gl.BLEND); @@ -86,10 +122,9 @@ export default class Renderer { } // Retrieve a given object (e.g. costume or speech bubble)'s skin. If it doesn't exist, make one. - _getSkin(obj) { - if (this._skins.has(obj)) { - return this._skins.get(obj); - } + _getSkin(obj: object): Skin { + const existingSkin = this._skins.get(obj); + if (existingSkin) return existingSkin; let skin; @@ -101,17 +136,17 @@ export default class Renderer { } } else { // If it's not a costume, assume it's a speech bubble. - skin = new SpeechBubbleSkin(this, obj); + skin = new SpeechBubbleSkin(this, obj as SpeechBubble); } this._skins.set(obj, skin); return skin; } // Retrieve the renderer-specific data object for a given sprite or clone. If it doesn't exist, make one. - _getDrawable(sprite) { - if (this._drawables.has(sprite)) { - return this._drawables.get(sprite); - } + _getDrawable(sprite: Sprite | Stage): Drawable { + const existingDrawable = this._drawables.get(sprite); + if (existingDrawable) return existingDrawable; + const drawable = new Drawable(this, sprite); this._drawables.set(sprite, drawable); return drawable; @@ -121,10 +156,18 @@ export default class Renderer { // * The framebuffer itself. // * The texture backing the framebuffer. // * The resolution (width and height) of the framebuffer. - _createFramebufferInfo(width, height, filtering, stencil = false) { + _createFramebufferInfo( + width: number, + height: number, + filtering: + | WebGLRenderingContext["NEAREST"] + | WebGLRenderingContext["LINEAR"], + stencil = false + ): FramebufferInfo { // Create an empty texture with this skin's dimensions. const gl = this.gl; const texture = gl.createTexture(); + if (texture === null) throw new Error("Could not create texture"); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); @@ -142,13 +185,16 @@ export default class Renderer { null ); + const framebuffer = gl.createFramebuffer(); + if (!framebuffer) throw new Error("Could not create framebuffer"); + // Create a framebuffer backed by said texture. This means we can draw onto the framebuffer, // and the results appear in the texture. const framebufferInfo = { texture, width, height, - framebuffer: gl.createFramebuffer() + framebuffer, }; this._setFramebuffer(framebufferInfo); gl.framebufferTexture2D( @@ -176,7 +222,7 @@ export default class Renderer { return framebufferInfo; } - _setShader(shader) { + _setShader(shader: Shader) { if (shader !== this._currentShader) { const gl = this.gl; gl.useProgram(shader.program); @@ -205,7 +251,7 @@ export default class Renderer { return false; } - _setFramebuffer(framebufferInfo) { + _setFramebuffer(framebufferInfo: FramebufferInfo | null) { if (framebufferInfo !== this._currentFramebuffer) { this._currentFramebuffer = framebufferInfo; if (framebufferInfo === null) { @@ -223,37 +269,40 @@ export default class Renderer { } } - setRenderTarget(renderTarget) { + setRenderTarget(renderTarget: HTMLElement | string | null) { if (typeof renderTarget === "string") { - renderTarget = document.querySelector(renderTarget); + renderTarget = document.querySelector(renderTarget) as HTMLElement; } this.renderTarget = renderTarget; - this.renderTarget.classList.add("leopard__project"); - this.renderTarget.style.width = `${this.project.stage.width}px`; - this.renderTarget.style.height = `${this.project.stage.height}px`; + if (!renderTarget) return; + renderTarget.classList.add("leopard__project"); + renderTarget.style.width = `${this.project.stage.width}px`; + renderTarget.style.height = `${this.project.stage.height}px`; - this.renderTarget.append(this.stage); + renderTarget.append(this.stage); } // Handles rendering of all layers (including stage, pen layer, sprites, and all clones) in proper order. - _renderLayers(layers, options = {}) { - options = Object.assign( - { - drawMode: ShaderManager.DrawModes.DEFAULT, - renderSpeechBubbles: true - }, - options - ); + _renderLayers( + layers?: Set, + optionsIn: { + filter?: (layer: Sprite | Stage | PenSkin) => boolean; + } & Partial = {} + ) { + const options = { + drawMode: ShaderManager.DrawModes.DEFAULT, + renderSpeechBubbles: true, + ...optionsIn, + }; // If we're given a list of layers, filter by that. // If we're given a filter function in the options, filter by that too. // If we're given both, then only include layers which match both. const shouldRestrictLayers = layers instanceof Set; - const shouldFilterLayers = typeof options.filter === "function"; - const shouldIncludeLayer = layer => + const shouldIncludeLayer = (layer: Sprite | Stage | PenSkin) => !( (shouldRestrictLayers && !layers.has(layer)) || - (shouldFilterLayers && !options.filter(layer)) + (typeof options.filter === "function" && !options.filter(layer)) ); // Stage @@ -345,7 +394,7 @@ export default class Renderer { this._renderLayers(); } - createStage(w, h) { + createStage(w: number, h: number) { const stage = document.createElement("canvas"); stage.width = w; stage.height = h; @@ -365,7 +414,10 @@ export default class Renderer { } // Calculate the transform matrix for a speech bubble attached to a sprite. - _calculateSpeechBubbleMatrix(spr, speechBubbleSkin) { + _calculateSpeechBubbleMatrix( + spr: Sprite, + speechBubbleSkin: SpeechBubbleSkin + ) { const sprBounds = this.getBoundingBox(spr); let x; if ( @@ -389,14 +441,14 @@ export default class Renderer { } _renderSkin( - skin, - drawMode, - matrix, - scale, - effects, - effectMask, - colorMask, - spriteColorId + skin: Skin, + drawMode: DrawMode, + matrix: MatrixType, + scale: number, + effects?: _EffectMap, + effectMask?: number, + colorMask?: [number, number, number, number], + spriteColorId?: number ) { const gl = this.gl; @@ -410,7 +462,7 @@ export default class Renderer { this._setShader(shader); gl.uniformMatrix3fv(shader.uniforms.u_transform, false, matrix); - if (effectBitmask !== 0) { + if (effectBitmask !== 0 && effects) { for (const effect of effectNames) { const effectVal = effects._effectValues[effect]; if (effectVal !== 0) @@ -419,7 +471,11 @@ export default class Renderer { // Pixelate effect needs the skin size if (effects._effectValues.pixelate !== 0) - gl.uniform2f(shader.uniforms.u_skinSize, skin.width, skin.height); + gl.uniform2f( + shader.uniforms.u_skinSize, + skin.width ?? 0, + skin.height ?? 0 + ); } gl.bindTexture(gl.TEXTURE_2D, skinTexture); @@ -428,26 +484,24 @@ export default class Renderer { // Enable color masking mode if set if (Array.isArray(colorMask)) - this.gl.uniform4fv(this._currentShader.uniforms.u_colorMask, colorMask); + this.gl.uniform4fv(shader.uniforms.u_colorMask, colorMask); // Used for mapping drawn sprites back to their indices in a list. // By looking at the color of a given pixel, we can tell which sprite is // the topmost one drawn on that pixel. - if (drawMode === ShaderManager.DrawModes.SPRITE_ID && typeof spriteColorId === "number") { - this.gl.uniform3fv( - this._currentShader.uniforms.u_spriteId, - idToColor(spriteColorId) - ); + if ( + drawMode === ShaderManager.DrawModes.SPRITE_ID && + typeof spriteColorId === "number" + ) { + this.gl.uniform3fv(shader.uniforms.u_spriteId, idToColor(spriteColorId)); } // Actually draw the skin this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); } - renderSprite(sprite, options) { - const spriteScale = Object.prototype.hasOwnProperty.call(sprite, "size") - ? sprite.size / 100 - : 1; + renderSprite(sprite: Sprite | Stage, options: RenderSpriteOptions) { + const spriteScale = "size" in sprite ? sprite.size / 100 : 1; this._renderSkin( this._getSkin(sprite.costume), @@ -463,9 +517,12 @@ export default class Renderer { if ( options.renderSpeechBubbles && sprite._speechBubble && - sprite._speechBubble.text !== "" + sprite._speechBubble.text !== "" && + sprite instanceof Sprite ) { - const speechBubbleSkin = this._getSkin(sprite._speechBubble); + const speechBubbleSkin = this._getSkin( + sprite._speechBubble + ) as SpeechBubbleSkin; this._renderSkin( speechBubbleSkin, @@ -476,16 +533,16 @@ export default class Renderer { } } - getTightBoundingBox(sprite) { + getTightBoundingBox(sprite: Sprite | Stage) { return this._getDrawable(sprite).getTightBoundingBox(); } - getBoundingBox(sprite) { + getBoundingBox(sprite: Sprite | Stage) { return Rectangle.fromMatrix(this._getDrawable(sprite).getMatrix()); } // Mask drawing in to only areas where this sprite is opaque. - _stencilSprite(spr, colorMask) { + _stencilSprite(spr: Sprite | Stage, colorMask?: Color) { const gl = this.gl; gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); @@ -507,11 +564,16 @@ export default class Renderer { // This, along with the above line, has the effect of not drawing anything to the color buffer, but // creating a "mask" in the stencil buffer that masks out all pixels where this sprite is transparent. - const opts = { + const opts: { + drawMode: DrawMode; + renderSpeechBubbles: boolean; + effectMask: number; + colorMask?: [number, number, number, number]; + } & RenderSpriteOptions = { drawMode: ShaderManager.DrawModes.SILHOUETTE, renderSpeechBubbles: false, // Ignore ghost effect - effectMask: ~effectBitmasks.ghost + effectMask: ~effectBitmasks.ghost, }; // If we mask in the color (for e.g. "color is touching color"), @@ -530,8 +592,13 @@ export default class Renderer { gl.colorMask(true, true, true, true); } - checkSpriteCollision(spr, targets, fast, sprColor) { - if (!spr.visible) return false; + checkSpriteCollision( + spr: Sprite | Stage, + targets: Set | (Sprite | Stage)[] | Sprite | Stage, + fast?: boolean, + sprColor?: Color + ) { + if ("visible" in spr && !spr.visible) return false; if (!(targets instanceof Set)) { if (targets instanceof Array) { targets = new Set(targets); @@ -581,7 +648,7 @@ export default class Renderer { this._renderLayers(targets, { drawMode: ShaderManager.DrawModes.SILHOUETTE, // Ignore ghost effect - effectMask: ~effectBitmasks.ghost + effectMask: ~effectBitmasks.ghost, }); const gl = this.gl; @@ -609,7 +676,11 @@ export default class Renderer { return false; } - checkColorCollision(spr, targetsColor, sprColor) { + checkColorCollision( + spr: Sprite | Stage, + targetsColor: Color, + sprColor?: Color + ) { const sprBox = Rectangle.copy( this.getBoundingBox(spr), __collisionBox @@ -631,8 +702,8 @@ export default class Renderer { this._stencilSprite(spr, sprColor); // Render the sprites to check that we're touching, which will now be masked in to the area of the first sprite. - this._renderLayers(null, { - filter: layer => layer !== spr + this._renderLayers(undefined, { + filter: (layer) => layer !== spr, }); // Make sure to disable the stencil test so as not to affect other rendering! @@ -667,13 +738,13 @@ export default class Renderer { } // Pick the topmost sprite at the given point (if one exists). - pick(sprites, point) { + pick(sprites: (Sprite | Stage)[], point: { x: number; y: number }) { this._setFramebuffer(this._collisionBuffer); const gl = this.gl; gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); - const spriteIndices = new Map(); + const spriteIndices = new Map(); for (let i = 0; i < sprites.length; i++) { spriteIndices.set(sprites[i], i); } @@ -682,7 +753,7 @@ export default class Renderer { effectMask: ~effectBitmasks.ghost, drawMode: ShaderManager.DrawModes.SPRITE_ID, // let's not use indexOf here--that would be O(n^2) - spriteColorId: (target) => spriteIndices.get(target) + spriteColorId: (target) => spriteIndices.get(target)!, }); const hoveredPixel = new Uint8Array(4); @@ -703,7 +774,11 @@ export default class Renderer { return sprites[index]; } - checkPointCollision(spr, point, fast) { + checkPointCollision( + spr: Sprite, + point: { x: number; y: number }, + fast?: boolean + ) { if (!spr.visible) return false; const box = this.getBoundingBox(spr); @@ -733,7 +808,12 @@ export default class Renderer { return hoveredPixel[3] !== 0; } - penLine(pt1, pt2, color, size) { + penLine( + pt1: { x: number; y: number }, + pt2: { x: number; y: number }, + color: Color, + size: number + ) { this._penSkin.penLine(pt1, pt2, color, size); } @@ -741,12 +821,13 @@ export default class Renderer { this._penSkin.clear(); } - stamp(spr) { + stamp(spr: Sprite | Stage) { this._setFramebuffer(this._penSkin._framebufferInfo); this._renderLayers(new Set([spr]), { renderSpeechBubbles: false }); } - displayAskBox(question) { + displayAskBox(question: string): Promise { + if (!this.renderTarget) return Promise.resolve(""); const askBox = document.createElement("form"); askBox.classList.add("leopard__askBox"); @@ -768,8 +849,8 @@ export default class Renderer { this.renderTarget.append(askBox); askInput.focus(); - return new Promise(resolve => { - askBox.addEventListener("submit", e => { + return new Promise((resolve) => { + askBox.addEventListener("submit", (e) => { e.preventDefault(); askBox.remove(); resolve(askInput.value); diff --git a/src/Sound.js b/src/Sound.ts similarity index 69% rename from src/Sound.js rename to src/Sound.ts index f414372..b3fda30 100644 --- a/src/Sound.js +++ b/src/Sound.ts @@ -1,7 +1,19 @@ import decodeADPCMAudio, { isADPCMData } from "./lib/decode-adpcm-audio.js"; export default class Sound { - constructor(name, url) { + name: string; + url: string; + + audioBuffer: AudioBuffer | null; + source: AudioBufferSourceNode | null; + playbackRate: number; + target?: AudioNode; + + _markDone?: () => void; + _doneDownloading?: (fromMoreRecentCall: boolean) => void; + + static _audioContext: AudioContext | undefined; + constructor(name: string, url: string) { this.name = name; this.url = url; @@ -10,11 +22,11 @@ export default class Sound { this.playbackRate = 1; // TODO: Remove this line; initiate downloads from somewhere else instead. - this.downloadMyAudioBuffer(); + void this.downloadMyAudioBuffer(); } get duration() { - return this.audioBuffer.duration; + return this.audioBuffer ? this.audioBuffer.duration : 0; } *start() { @@ -51,7 +63,7 @@ export default class Sound { // finish playing. Of course, the latest call returns true, and so the // containing playUntilDone() (if present) knows to wait. const oldDoneDownloading = this._doneDownloading; - this._doneDownloading = fromMoreRecentCall => { + this._doneDownloading = (fromMoreRecentCall) => { if (fromMoreRecentCall) { isLatestCallToStart = false; } else { @@ -77,7 +89,7 @@ export default class Sound { // If we failed to download the audio buffer, just stop here - the sound will // never play, so it doesn't make sense to wait for it. - if (!this.audioBuffer) { + if (!this.audioBuffer || !this.source) { return; } @@ -118,27 +130,32 @@ export default class Sound { downloadMyAudioBuffer() { return fetch(this.url) - .then(body => body.arrayBuffer()) - .then(arrayBuffer => { + .then((body) => body.arrayBuffer()) + .then((arrayBuffer) => { if (isADPCMData(arrayBuffer)) { return decodeADPCMAudio(arrayBuffer, Sound.audioContext).catch( - error => { + (error: Error) => { console.warn( - `Failed to load sound "${this.name}" - will not play:\n` + error + `Failed to load sound "${this.name}" - will not play:\n` + + error.toString() ); return null; } ); } else { - return new Promise((resolve, reject) => { - Sound.audioContext.decodeAudioData(arrayBuffer, resolve, reject); + return new Promise((resolve: DecodeSuccessCallback, reject) => { + void Sound.audioContext.decodeAudioData( + arrayBuffer, + resolve, + reject + ); }); } }) - .then(audioBuffer => { + .then((audioBuffer) => { this.audioBuffer = audioBuffer; if (this._doneDownloading) { - this._doneDownloading(); + this._doneDownloading(false); } return audioBuffer; }); @@ -164,7 +181,7 @@ export default class Sound { this.source.start(Sound.audioContext.currentTime); } - connect(target) { + connect(target: AudioNode) { if (target !== this.target) { this.target = target; if (this.source) { @@ -174,36 +191,140 @@ export default class Sound { } } - setPlaybackRate(value) { + setPlaybackRate(value: number) { this.playbackRate = value; if (this.source) { this.source.playbackRate.value = value; } } - isConnectedTo(target) { + isConnectedTo(target: AudioNode) { return this.target === target; } // Note: "this" refers to the Sound class in static functions. - static get audioContext() { - this._setupAudioContext(); - return this._audioContext; - } - - static _setupAudioContext() { + static get audioContext(): AudioContext { if (!this._audioContext) { - const AudioContext = window.AudioContext || window.webkitAudioContext; + const AudioContext = + window.AudioContext || + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + ((window as any).webkitAudioContext as AudioContext); this._audioContext = new AudioContext(); } + return this._audioContext; } - static decodeADPCMAudio(audioBuffer) { + static decodeADPCMAudio(audioBuffer: ArrayBuffer) { return decodeADPCMAudio(audioBuffer, this.audioContext); } } +// Instead of creating a basic Effect class and then implementing a subclass +// for each effect type, we use a simplified object-descriptor style. +// The makeNodes() function returns an object which is passed on to set(), so +// that effects are able to access a variety of nodes (or other values, if +// necessary) required to execute the desired effect. +// +// The code in makeNodes as well as the general definition for each effect is +// all graciously based on LLK's scratch-audio library. +// +// The initial value of an effect should always be the value at which the +// sound is not affected at all - i.e, it would be the same if the effect +// nodes were completely disconnected from the chain or otherwise had never +// been applied. This allows for clean discarding of effect nodes when returned +// to the initial value. +// +// The order of this array matches AudioEngine's effects list in scratch-audio. +// Earlier in the list is closer to the EffectChain input node; later is closer +// to its target (output). Note that a non-"patch" effect's position in the +// array has no bearing on effect behavior, since it isn't part of the chain +// system. +// +// Note that this descriptor list is fairly easy to build on, if we'd like to +// add more audio effects in the future. (Scratch used to have more, but they +// were removed - see commit ff6cd4a - because they depended on an external +// library and were too processor-intensive to support on some devices.) +const PanEffect: EffectDescriptor< + true, + "pan", + { leftGain: GainNode; rightGain: GainNode } +> = { + name: "pan", + initial: 0, + minimum: -100, + maximum: 100, + isPatch: true, + makeNodes() { + const aCtx = Sound.audioContext; + const input = aCtx.createGain(); + const leftGain = aCtx.createGain(); + const rightGain = aCtx.createGain(); + const channelMerger = aCtx.createChannelMerger(2); + const output = channelMerger; + input.connect(leftGain); + input.connect(rightGain); + leftGain.connect(channelMerger, 0, 0); + rightGain.connect(channelMerger, 0, 1); + return { input, output, leftGain, rightGain, channelMerger }; + }, + set(value, { leftGain, rightGain }) { + const p = (value + 100) / 200; + const leftVal = Math.cos((p * Math.PI) / 2); + const rightVal = Math.sin((p * Math.PI) / 2); + const { currentTime } = Sound.audioContext; + const { decayWait, decayDuration } = EffectChain; + leftGain.gain.setTargetAtTime( + leftVal, + currentTime + decayWait, + decayDuration + ); + rightGain.gain.setTargetAtTime( + rightVal, + currentTime + decayWait, + decayDuration + ); + }, +} as const; +const PitchEffect: EffectDescriptor = { + name: "pitch", + initial: 0, + isPatch: false, + set(value, sound) { + const interval = value / 10; + const ratio = Math.pow(2, interval / 12); + sound.setPlaybackRate(ratio); + }, +} as const; +const VolumeEffect: EffectDescriptor = { + name: "volume", + initial: 100, + minimum: 0, + maximum: 100, + resetOnStart: false, + resetOnClone: true, + isPatch: true, + makeNodes() { + const node = Sound.audioContext.createGain(); + return { + input: node, + output: node, + node, + }; + }, + set(value, { node }) { + node.gain.linearRampToValueAtTime( + value / 100, + Sound.audioContext.currentTime + EffectChain.decayDuration + ); + }, +} as const; + +const effectDescriptors = [PanEffect, PitchEffect, VolumeEffect] as const; +type EffectName = typeof effectDescriptors[number]["name"]; + +type EffectChainConfig = { getNonPatchSoundList: () => Sound[] }; + export class EffectChain { // The code in this class is functionally comparable to the class of the same // name in the scratch-audio library, but is completely rewritten and follows @@ -211,7 +332,21 @@ export class EffectChain { // a portable way to store the effect chain, independent of the audio sources // it affects. - constructor(config) { + // TODO: stop storing config; we just use getNonPatchSoundList directly + config: EffectChainConfig; + inputNode: AudioNode; + getNonPatchSoundList: () => Sound[]; + effectValues!: Record; + effectNodes: { + [T in typeof effectDescriptors[number] as T["name"]]?: ReturnType< + // We need to infer this type here, I think + // eslint-disable-next-line @typescript-eslint/no-unused-vars + T extends PatchDescriptor ? T["makeNodes"] : never + >; + }; + target?: AudioNode; + + constructor(config: EffectChainConfig) { const { getNonPatchSoundList } = config; this.config = config; @@ -241,8 +376,8 @@ export class EffectChain { if (this.effectValues) { for (const [name, initialValue] of Object.entries( EffectChain.getInitialEffectValues() - )) { - if (EffectChain.getEffectDescriptor(name).reset !== false) { + ) as [EffectName, number][]) { + if (EffectChain.getEffectDescriptor(name).resetOnStart !== false) { this.setEffectValue(name, initialValue); } } @@ -251,7 +386,7 @@ export class EffectChain { } } - updateAudioEffect(name) { + updateAudioEffect(name: EffectName) { const descriptor = EffectChain.getEffectDescriptor(name); if (!descriptor) { @@ -267,15 +402,22 @@ export class EffectChain { // who have existent nodes. This means we'll skip non-patch effects as // well as effects are set to their initial value. - let next = descriptor; + let nextDescriptor: EffectDescriptorBase = descriptor; do { - next = EffectChain.getNextEffectDescriptor(next.name); - } while (next && !this.effectNodes[next.name]); + nextDescriptor = EffectChain.getNextEffectDescriptor( + nextDescriptor.name + )!; + } while (nextDescriptor && !this.effectNodes[nextDescriptor.name]); - let previous = descriptor; + let previousDescriptor: EffectDescriptorBase = descriptor; do { - previous = EffectChain.getPreviousEffectDescriptor(previous.name); - } while (previous && !this.effectNodes[previous.name]); + previousDescriptor = EffectChain.getPreviousEffectDescriptor( + previousDescriptor.name + )!; + } while ( + previousDescriptor && + !this.effectNodes[previousDescriptor.name] + ); // If we have previous and next values available, they'll currently be // the corresponding descriptors. But we only ever need to access the @@ -283,12 +425,14 @@ export class EffectChain { // with the actual objects containing the effect's nodes here to simplify // later code. - if (next) { - next = this.effectNodes[next.name]; + let next; + if (nextDescriptor) { + next = this.effectNodes[nextDescriptor.name]; } - if (previous) { - next = this.effectNodes[previous.name]; + let previous; + if (previousDescriptor) { + previous = this.effectNodes[previousDescriptor.name]; } // If there is no preceding or following effect which has existent nodes, @@ -321,10 +465,10 @@ export class EffectChain { // node as both its input and output.) Other effects are more complex. // The code in this block controls the actual chaining behavior of // EffectChain, assuring that all effects form a clean chain. - let nodes = this.effectNodes[descriptor.name]; + let nodes = this.effectNodes[descriptor.name]!; if (!nodes && value !== descriptor.initial) { nodes = descriptor.makeNodes(); - this.effectNodes[descriptor.name] = nodes; + this.effectNodes[descriptor.name] = nodes as never; // Connect the previous effect, or, if there is none, the EffectChain // input, to this effect. Also disconnect it from whatever it was @@ -370,7 +514,7 @@ export class EffectChain { delete this.effectNodes[name]; } } else { - descriptor.set(value, nodes); + descriptor.set(value, nodes as never); } } else { // Non-"patch" effects operate directly on Sound objects, accessing @@ -382,7 +526,7 @@ export class EffectChain { } } - connect(target) { + connect(target: AudioNode) { this.target = target; // All the code here is basically the same as what's written in @@ -390,13 +534,17 @@ export class EffectChain { // disconnect the final output in the chain - which may be the input // node - and then connect it to the newly specified target. - let last = EffectChain.getLastEffectDescriptor(); + let lastDescriptor: EffectDescriptorBase = + EffectChain.getLastEffectDescriptor(); do { - last = EffectChain.getPreviousEffectDescriptor(last.name); - } while (last && !this.effectNodes[last.name]); - - if (last) { - last = this.effectNodes[last.name]; + lastDescriptor = EffectChain.getPreviousEffectDescriptor( + lastDescriptor.name + )!; + } while (lastDescriptor && !this.effectNodes[lastDescriptor.name]); + + let last; + if (lastDescriptor) { + last = this.effectNodes[lastDescriptor.name]!; } else { last = { output: this.inputNode }; } @@ -405,7 +553,7 @@ export class EffectChain { last.output.connect(target); } - setEffectValue(name, value) { + setEffectValue(name: EffectName, value: number | string | boolean) { value = Number(value); if ( name in this.effectValues && @@ -418,7 +566,7 @@ export class EffectChain { } } - changeEffectValue(name, value) { + changeEffectValue(name: EffectName, value: number | string | boolean) { value = Number(value); if (name in this.effectValues && !isNaN(value) && value !== 0) { this.effectValues[name] += value; @@ -427,68 +575,83 @@ export class EffectChain { } } - clampEffectValue(name) { + clampEffectValue(name: EffectName) { // Not all effects are clamped (pitch, for example); it's also possible to // specify only a minimum or maximum bound, instead of both. const descriptor = EffectChain.getEffectDescriptor(name); let value = this.effectValues[name]; - if ("minimum" in descriptor && value < descriptor.minimum) { + if (typeof descriptor.minimum === "number" && value < descriptor.minimum) { value = descriptor.minimum; - } else if ("maximum" in descriptor && value > descriptor.maximum) { + } else if ( + typeof descriptor.maximum === "number" && + value > descriptor.maximum + ) { value = descriptor.maximum; } this.effectValues[name] = value; } - getEffectValue(name) { + getEffectValue(name: EffectName): number { return this.effectValues[name] || 0; } - clone(newConfig) { + clone(newConfig: EffectChainConfig) { const newEffectChain = new EffectChain( Object.assign({}, this.config, newConfig) ); - for (const [name, value] of Object.entries(this.effectValues)) { + for (const [name, value] of Object.entries(this.effectValues) as [ + EffectName, + number + ][]) { const descriptor = EffectChain.getEffectDescriptor(name); if (!descriptor.resetOnClone) { newEffectChain.setEffectValue(name, value); } } - newEffectChain.connect(this.target); + if (this.target) newEffectChain.connect(this.target); return newEffectChain; } - applyToSound(sound) { + applyToSound(sound: Sound) { sound.connect(this.inputNode); - for (const [name, value] of Object.entries(this.effectValues)) { + for (const [name, value] of Object.entries(this.effectValues) as [ + EffectName, + number + ][]) { const descriptor = EffectChain.getEffectDescriptor(name); if (!descriptor.isPatch) { - descriptor.set(value, sound); + (descriptor as PatchlessDescriptor).set(value, sound); } } } - isTargetOf(sound) { + isTargetOf(sound: Sound) { return sound.isConnectedTo(this.inputNode); } - static getInitialEffectValues() { + static getInitialEffectValues(): Record { // This would be an excellent place to use Object.fromEntries, but that // function has been implemented in only the latest of a few modern // browsers. :P - const initials = {}; + const initials: Partial> = {}; for (const { name, initial } of this.effectDescriptors) { initials[name] = initial; } - return initials; + return initials as Record; } - static getEffectDescriptor(name) { - return this.effectDescriptors.find(descriptor => descriptor.name === name); + static getEffectDescriptor( + name: EffectName + ): typeof EffectChain["effectDescriptors"][number] { + // We know this is non-null because this.effectDescriptors has every effect descriptor in it. + // TODO: use a Record? + return this.effectDescriptors.find( + (descriptor) => descriptor.name === name + )!; } static getFirstEffectDescriptor() { @@ -499,7 +662,7 @@ export class EffectChain { return this.effectDescriptors[this.effectDescriptors.length - 1]; } - static getNextEffectDescriptor(name) { + static getNextEffectDescriptor(name: EffectName) { // .find() provides three values to its passed function: the value of the // current item, that item's index, and the array on which .find() is // operating. In this case, we're only concerned with the index. @@ -514,7 +677,7 @@ export class EffectChain { .find((_, i) => this.effectDescriptors[i].name === name); } - static getPreviousEffectDescriptor(name) { + static getPreviousEffectDescriptor(name: EffectName) { // This function's a little simpler, since it doesn't involve shifting the // list. We still use slice(), but this time simply to cut off the last // item; that item will never come before any other, after all. We search @@ -526,110 +689,45 @@ export class EffectChain { .slice(0, -1) .find((_, i) => this.effectDescriptors[i + 1].name === name); } -} -// These are constant values which can be affected to tweak the way effects -// are applied. They match the values used in Scratch 3.0. -EffectChain.decayDuration = 0.025; -EffectChain.decayWait = 0.05; + // These are constant values which can be affected to tweak the way effects + // are applied. They match the values used in Scratch 3.0. + static decayDuration = 0.025; + static decayWait = 0.05; -// Instead of creating a basic Effect class and then implementing a subclass -// for each effect type, we use a simplified object-descriptor style. -// The makeNodes() function returns an object which is passed on to set(), so -// that effects are able to access a variety of nodes (or other values, if -// necessary) required to execute the desired effect. -// -// The code in makeNodes as well as the general definition for each effect is -// all graciously based on LLK's scratch-audio library. -// -// The initial value of an effect should always be the value at which the -// sound is not affected at all - i.e, it would be the same if the effect -// nodes were completely disconnected from the chain or otherwise had never -// been applied. This allows for clean discarding of effect nodes when returned -// to the initial value. -// -// The order of this array matches AudioEngine's effects list in scratch-audio. -// Earlier in the list is closer to the EffectChain input node; later is closer -// to its target (output). Note that a non-"patch" effect's position in the -// array has no bearing on effect behavior, since it isn't part of the chain -// system. -// -// Note that this descriptor list is fairly easy to build on, if we'd like to -// add more audio effects in the future. (Scratch used to have more, but they -// were removed - see commit ff6cd4a - because they depended on an external -// library and were too processor-intensive to support on some devices.) -EffectChain.effectDescriptors = [ - { - name: "pan", - initial: 0, - minimum: -100, - maximum: 100, - isPatch: true, - makeNodes() { - const aCtx = Sound.audioContext; - const input = aCtx.createGain(); - const leftGain = aCtx.createGain(); - const rightGain = aCtx.createGain(); - const channelMerger = aCtx.createChannelMerger(2); - const output = channelMerger; - input.connect(leftGain); - input.connect(rightGain); - leftGain.connect(channelMerger, 0, 0); - rightGain.connect(channelMerger, 0, 1); - return { input, output, leftGain, rightGain, channelMerger }; - }, - set(value, { input, output, leftGain, rightGain }) { - const p = (value + 100) / 200; - const leftVal = Math.cos((p * Math.PI) / 2); - const rightVal = Math.sin((p * Math.PI) / 2); - const { currentTime } = Sound.audioContext; - const { decayWait, decayDuration } = EffectChain; - leftGain.gain.setTargetAtTime( - leftVal, - currentTime + decayWait, - decayDuration - ); - rightGain.gain.setTargetAtTime( - rightVal, - currentTime + decayWait, - decayDuration - ); - } - }, - { - name: "pitch", - initial: 0, - isPatch: false, - set(value, sound) { - const interval = value / 10; - const ratio = Math.pow(2, interval / 12); - sound.setPlaybackRate(ratio); - } - }, - { - name: "volume", - initial: 100, - minimum: 0, - maximum: 100, - resetOnStart: false, - resetOnClone: true, - isPatch: true, - makeNodes() { - const node = Sound.audioContext.createGain(); - return { - input: node, - output: node, - node - }; - }, - set(value, { node }) { - node.gain.linearRampToValueAtTime( - value / 100, - Sound.audioContext.currentTime + EffectChain.decayDuration - ); - } - } -]; + static effectDescriptors = effectDescriptors; +} + +type EffectDescriptorBase = { + name: Name; + initial: number; + minimum?: number; + maximum?: number; + resetOnStart?: boolean; + resetOnClone?: boolean; +}; + +type PatchlessDescriptor = { + isPatch: false; + set: (value: number, sound: Sound) => void; +} & EffectDescriptorBase; + +type PatchDescriptor = { + isPatch: true; + makeNodes: () => Nodes & { input: AudioNode; output: AudioNode }; + set: ( + value: number, + nodes: Nodes & { input: AudioNode; output: AudioNode } + ) => void; +} & EffectDescriptorBase; + +type EffectDescriptor< + isPatch extends boolean, + Name extends string, + Nodes extends isPatch extends true ? object : never +> = isPatch extends true + ? PatchDescriptor + : PatchlessDescriptor; export class AudioEffectMap { // This class provides a simple interface for setting and getting audio @@ -637,14 +735,16 @@ export class AudioEffectMap { // for graphic effects). It takes an EffectChain and automatically generates // properties according to the names of the effect descriptors, acting with // the EffectChain's API when accessed. + effectChain: EffectChain; - constructor(effectChain) { + constructor(effectChain: EffectChain) { this.effectChain = effectChain; for (const { name } of EffectChain.effectDescriptors) { Object.defineProperty(this, name, { get: () => effectChain.getEffectValue(name), - set: value => effectChain.setEffectValue(name, value) + set: (value: string | number | boolean) => + effectChain.setEffectValue(name, value), }); } } diff --git a/src/Sprite.js b/src/Sprite.ts similarity index 62% rename from src/Sprite.js rename to src/Sprite.ts index 62aff76..db482ea 100644 --- a/src/Sprite.js +++ b/src/Sprite.ts @@ -1,26 +1,52 @@ import Color from "./Color.js"; import Trigger from "./Trigger.js"; import Sound, { EffectChain, AudioEffectMap } from "./Sound.js"; +import Costume from "./Costume.js"; +import type Project from "./Project.js"; +import type Watcher from "./Watcher.js"; import { effectNames } from "./renderer/effectInfo.js"; + +type Effects = { + [x in typeof effectNames[number]]: number; +}; + // This is a wrapper to allow the enabled effects in a sprite to be used as a Map key. // By setting an effect, the bitmask is updated as well. // This allows the bitmask to be used to uniquely identify a set of enabled effects. -class _EffectMap { +export class _EffectMap implements Effects { + _bitmask: number; + _effectValues: Record; + // TODO: TypeScript can't automatically infer these + color!: number; + fisheye!: number; + whirl!: number; + pixelate!: number; + mosaic!: number; + brightness!: number; + ghost!: number; + constructor() { this._bitmask = 0; - this._effectValues = {}; + this._effectValues = { + color: 0, + fisheye: 0, + whirl: 0, + pixelate: 0, + mosaic: 0, + brightness: 0, + ghost: 0, + }; for (let i = 0; i < effectNames.length; i++) { const effectName = effectNames[i]; - this._effectValues[effectName] = 0; Object.defineProperty(this, effectName, { get: () => { return this._effectValues[effectName]; }, - set: val => { + set: (val: number) => { this._effectValues[effectName] = val; if (val === 0) { @@ -30,29 +56,61 @@ class _EffectMap { // Otherwise, set its bit to 1. this._bitmask = this._bitmask | (1 << i); } - } + }, }); } } _clone() { const m = new _EffectMap(); - for (const effectName of Object.keys(this._effectValues)) { + for (const effectName of Object.keys( + this._effectValues + ) as (keyof typeof this._effectValues)[]) { m[effectName] = this[effectName]; } return m; } clear() { - for (const effectName of Object.keys(this._effectValues)) { + for (const effectName of Object.keys( + this._effectValues + ) as (keyof typeof this._effectValues)[]) { this._effectValues[effectName] = 0; } this._bitmask = 0; } } -class SpriteBase { - constructor(initialConditions, vars = {}) { +export type SpeechBubble = { + text: string; + style: "say" | "think"; + timeout: number | null; +}; + +type InitialConditions = { + costumeNumber: number; + layerOrder?: number; +}; + +abstract class SpriteBase { + _project: Project | null; + + _costumeNumber: number; + _layerOrder: number; + triggers: Trigger[]; + watchers: Partial>; + costumes: Costume[]; + sounds: Sound[]; + + effectChain: EffectChain; + effects: _EffectMap; + audioEffects: AudioEffectMap; + + _speechBubble?: SpeechBubble; + + _vars: Vars; + + constructor(initialConditions: InitialConditions, vars: Vars) { this._project = null; const { costumeNumber, layerOrder = 0 } = initialConditions; @@ -65,26 +123,26 @@ class SpriteBase { this.sounds = []; this.effectChain = new EffectChain({ - getNonPatchSoundList: this.getSoundsPlayedByMe.bind(this) + getNonPatchSoundList: this.getSoundsPlayedByMe.bind(this), }); this.effectChain.connect(Sound.audioContext.destination); this.effects = new _EffectMap(); this.audioEffects = new AudioEffectMap(this.effectChain); - this._vars = vars; + this._vars = vars ?? {}; } getSoundsPlayedByMe() { - return this.sounds.filter(sound => this.effectChain.isTargetOf(sound)); + return this.sounds.filter((sound) => this.effectChain.isTargetOf(sound)); } get stage() { - return this._project.stage; + return this._project?.stage; } get sprites() { - return this._project.sprites; + return this._project?.sprites; } get vars() { @@ -97,15 +155,21 @@ class SpriteBase { set costumeNumber(number) { this._costumeNumber = this.wrapClamp(number, 1, this.costumes.length); - if (this.fireBackdropChanged) this.fireBackdropChanged(); } - set costume(costume) { + set costume(costume: number | string | Costume) { + if (costume instanceof Costume) { + const costumeIndex = this.costumes.indexOf(costume); + if (costumeIndex > -1) { + this.costumeNumber = costumeIndex + 1; + } + } if (typeof costume === "number") { - this.costumeNumber = costume; + if (!isNaN(costume)) this.costumeNumber = costume; + return; } if (typeof costume === "string") { - const index = this.costumes.findIndex(c => c.name === costume); + const index = this.costumes.findIndex((c) => c.name === costume); if (index > -1) { this.costumeNumber = index + 1; } else { @@ -141,7 +205,10 @@ class SpriteBase { } default: { - if (!(isNaN(costume) || costume.trim().length === 0)) { + if ( + Number.isFinite(Number(costume)) && + costume.trim().length !== 0 + ) { this.costumeNumber = Number(costume); } } @@ -150,47 +217,31 @@ class SpriteBase { } } - get costume() { + get costume(): Costume { return this.costumes[this.costumeNumber - 1]; } - moveAhead(value = Infinity) { - if (typeof value === "number") { - this._project.changeSpriteLayer(this, value); - } else { - this._project.changeSpriteLayer(this, 1, value); - } - } - - moveBehind(value = Infinity) { - if (typeof value === "number") { - this._project.changeSpriteLayer(this, -value); - } else { - this._project.changeSpriteLayer(this, -1, value); - } - } - - degToRad(deg) { + degToRad(deg: number) { return (deg * Math.PI) / 180; } - radToDeg(rad) { + radToDeg(rad: number) { return (rad * 180) / Math.PI; } - degToScratch(deg) { + degToScratch(deg: number) { return -deg + 90; } - scratchToDeg(scratchDir) { + scratchToDeg(scratchDir: number) { return -scratchDir + 90; } - radToScratch(rad) { + radToScratch(rad: number) { return this.degToScratch(this.radToDeg(rad)); } - scratchToRad(scratchDir) { + scratchToRad(scratchDir: number) { return this.degToRad(this.scratchToDeg(scratchDir)); } @@ -210,7 +261,7 @@ class SpriteBase { } // Wrap rotation from -180 to 180. - normalizeDeg(deg) { + normalizeDeg(deg: number) { // This is a pretty big math expression, but it's necessary because in JavaScript, // the % operator means "remainder", not "modulo", and so negative numbers won't "wrap around". // See https://web.archive.org/web/20090717035140if_/javascript.about.com/od/problemsolving/a/modulobug.htm @@ -228,7 +279,7 @@ class SpriteBase { } // Given a generator function, return a version of it that runs in "warp mode" (no yields). - warp(procedure) { + warp(procedure: GeneratorFunction): (...args: unknown[]) => void { const bound = procedure.bind(this); return (...args) => { const inst = bound(...args); @@ -236,7 +287,8 @@ class SpriteBase { }; } - random(a, b) { + // TODO: this should also take strings so rand("0.0", "1.0") returns a random float like Scratch + random(a: number, b: number) { const min = Math.min(a, b); const max = Math.max(a, b); if (min % 1 === 0 && max % 1 === 0) { @@ -245,8 +297,8 @@ class SpriteBase { return Math.random() * (max - min) + min; } - *wait(secs) { - let endTime = new Date(); + *wait(secs: number) { + const endTime = new Date(); endTime.setMilliseconds(endTime.getMilliseconds() + secs * 1000); while (new Date() < endTime) { yield; @@ -254,22 +306,22 @@ class SpriteBase { } get mouse() { - return this._project.input.mouse; + return this._project?.input.mouse; } - keyPressed(name) { - return this._project.input.keyPressed(name); + keyPressed(name: string) { + return this._project?.input.keyPressed(name); } get timer() { - return this._project.timer; + return this._project?.timer; } restartTimer() { - this._project.restartTimer(); + this._project?.restartTimer(); } - *startSound(soundName) { + *startSound(soundName: string) { const sound = this.getSound(soundName); if (sound) { this.effectChain.applyToSound(sound); @@ -277,7 +329,7 @@ class SpriteBase { } } - *playSoundUntilDone(soundName) { + *playSoundUntilDone(soundName: string) { const sound = this.getSound(soundName); if (sound) { sound.connect(this.effectChain.inputNode); @@ -286,16 +338,16 @@ class SpriteBase { } } - getSound(soundName) { + getSound(soundName: string) { if (typeof soundName === "number") { return this.sounds[(soundName - 1) % this.sounds.length]; } else { - return this.sounds.find(s => s.name === soundName); + return this.sounds.find((s) => s.name === soundName); } } stopAllSounds() { - this._project.stopAllSounds(); + this._project?.stopAllSounds(); } stopAllOfMySounds() { @@ -304,13 +356,13 @@ class SpriteBase { } } - broadcast(name) { - return this._project.fireTrigger(Trigger.BROADCAST, { name }); + broadcast(name: string) { + return this._project?.fireTrigger(Trigger.BROADCAST, { name }); } - *broadcastAndWait(name) { + *broadcastAndWait(name: string) { let running = true; - this.broadcast(name).then(() => { + void this.broadcast(name)?.then(() => { running = false; }); @@ -320,16 +372,16 @@ class SpriteBase { } clearPen() { - this._project.renderer.clearPen(); + this._project?.renderer.clearPen(); } - *askAndWait(question) { - if (this._speechBubble) { + *askAndWait(question: string) { + if (this._speechBubble && this instanceof Sprite) { this.say(""); } let done = false; - this._project.askAndWait(question).then(() => { + void this._project?.askAndWait(question).then(() => { done = true; }); @@ -337,11 +389,11 @@ class SpriteBase { } get answer() { - return this._project.answer; + return this._project?.answer; } get loudness() { - return this._project.loudness; + return this._project?.loudness; } toNumber(value) { @@ -441,9 +493,39 @@ class SpriteBase { } } -export class Sprite extends SpriteBase { - constructor(initialConditions, ...args) { - super(initialConditions, ...args); +type RotationStyle = + typeof Sprite["RotationStyle"][keyof typeof Sprite["RotationStyle"]]; + +type SpriteInitialConditions = { + x: number; + y: number; + direction: number; + rotationStyle?: RotationStyle; + costumeNumber: number; + size: number; + visible: boolean; + penDown?: boolean; + penSize?: number; + penColor?: Color; +}; + +export class Sprite extends SpriteBase { + _x: number; + _y: number; + _direction: number; + rotationStyle: RotationStyle; + size: number; + visible: boolean; + + parent: Sprite | null; + clones: Sprite[]; + + _penDown: boolean; + penSize: number; + _penColor: Color; + + constructor(initialConditions: SpriteInitialConditions, vars: Vars) { + super(initialConditions, vars); const { x, @@ -455,7 +537,7 @@ export class Sprite extends SpriteBase { visible, penDown, penSize, - penColor + penColor, } = initialConditions; this._x = x; @@ -476,19 +558,20 @@ export class Sprite extends SpriteBase { this._speechBubble = { text: "", style: "say", - timeout: null + timeout: null, }; } createClone() { const clone = Object.assign( - Object.create(Object.getPrototypeOf(this)), + Object.create(Object.getPrototypeOf(this) as object) as Sprite, this ); clone._project = this._project; clone.triggers = this.triggers.map( - trigger => new Trigger(trigger.trigger, trigger.options, trigger._script) + (trigger) => + new Trigger(trigger.trigger, trigger.options, trigger._script) ); clone.costumes = this.costumes; clone.sounds = this.sounds; @@ -497,19 +580,20 @@ export class Sprite extends SpriteBase { clone._speechBubble = { text: "", style: "say", - timeout: null + timeout: null, }; clone.effects = this.effects._clone(); // Clones inherit audio effects from the original sprite, for some reason. // Couldn't explain it, but that's the behavior in Scratch 3.0. - let original = this; + // eslint-disable-next-line @typescript-eslint/no-this-alias + let original: Sprite = this; while (original.parent) { original = original.parent; } clone.effectChain = original.effectChain.clone({ - getNonPatchSoundList: clone.getSoundsPlayedByMe.bind(clone) + getNonPatchSoundList: clone.getSoundsPlayedByMe.bind(clone), }); // Make a new audioEffects interface which acts on the cloned effect chain. @@ -520,26 +604,29 @@ export class Sprite extends SpriteBase { this.clones.push(clone); // Trigger CLONE_START: - const triggers = clone.triggers.filter(tr => + const triggers = clone.triggers.filter((tr) => tr.matches(Trigger.CLONE_START, {}, clone) ); - this._project._startTriggers( - triggers.map(trigger => ({ trigger, target: clone })) + void this._project?._startTriggers( + triggers.map((trigger) => ({ trigger, target: clone })) ); } deleteThisClone() { if (this.parent === null) return; - this.parent.clones = this.parent.clones.filter(clone => clone !== this); + this.parent.clones = this.parent.clones.filter((clone) => clone !== this); - this._project.runningTriggers = this._project.runningTriggers.filter( - ({ target }) => target !== this - ); + if (this._project) { + this._project.runningTriggers = this._project.runningTriggers.filter( + ({ target }) => target !== this + ); + } } - andClones() { - return [this, ...this.clones.flatMap(clone => clone.andClones())]; + // TODO: is this necessary now that the clone hierarchy seems to be flattened? + andClones(): Sprite[] { + return [this, ...this.clones.flatMap((clone) => clone.andClones())]; } get direction() { @@ -550,10 +637,10 @@ export class Sprite extends SpriteBase { this._direction = this.normalizeDeg(dir); } - goto(x, y) { + goto(x: number, y: number) { if (x === this.x && y === this.y) return; - if (this.penDown) { + if (this.penDown && this._project) { this._project.renderer.penLine( { x: this._x, y: this._y }, { x, y }, @@ -582,7 +669,7 @@ export class Sprite extends SpriteBase { this.goto(this._x, y); } - move(dist) { + move(dist: number) { const moveDir = this.scratchToRad(this.direction); this.goto( @@ -591,8 +678,8 @@ export class Sprite extends SpriteBase { ); } - *glide(seconds, x, y) { - const interpolate = (a, b, t) => a + (b - a) * t; + *glide(seconds: number, x: number, y: number) { + const interpolate = (a: number, b: number, t: number) => a + (b - a) * t; const startTime = new Date(); const startX = this._x; @@ -600,18 +687,34 @@ export class Sprite extends SpriteBase { let t; do { - t = (new Date() - startTime) / (seconds * 1000); + t = (new Date().getTime() - startTime.getTime()) / (seconds * 1000); this.goto(interpolate(startX, x, t), interpolate(startY, y, t)); yield; } while (t < 1); } + moveAhead(value = Infinity) { + if (typeof value === "number") { + this._project?.changeSpriteLayer(this, value); + } else { + this._project?.changeSpriteLayer(this, 1, value); + } + } + + moveBehind(value = Infinity) { + if (typeof value === "number") { + this._project?.changeSpriteLayer(this, -value); + } else { + this._project?.changeSpriteLayer(this, -1, value); + } + } + get penDown() { return this._penDown; } set penDown(penDown) { - if (penDown) { + if (penDown && this._project) { this._project.renderer.penLine( { x: this.x, y: this.y }, { x: this.x, y: this.y }, @@ -626,21 +729,23 @@ export class Sprite extends SpriteBase { return this._penColor; } - set penColor(color) { + set penColor(color: unknown) { if (color instanceof Color) { this._penColor = color; } else { console.error( - `${color} is not a valid penColor. Try using the Color class!` + `${String(color)} is not a valid penColor. Try using the Color class!` ); } } stamp() { - this._project.renderer.stamp(this); + this._project?.renderer.stamp(this); } - touching(target, fast = false) { + touching(target: "mouse" | "edge" | Sprite | Stage, fast = false): boolean { + if (!this._project || !this.mouse || !this.stage) return false; + if (typeof target === "string") { switch (target) { case "mouse": @@ -648,7 +753,7 @@ export class Sprite extends SpriteBase { this, { x: this.mouse.x, - y: this.mouse.y + y: this.mouse.y, }, fast ); @@ -665,6 +770,7 @@ export class Sprite extends SpriteBase { } default: console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Cannot find target "${target}" in "touching". Did you mean to pass a sprite class instead?` ); return false; @@ -676,9 +782,10 @@ export class Sprite extends SpriteBase { return this._project.renderer.checkSpriteCollision(this, target, fast); } - colorTouching(color, target) { + colorTouching(color: Color, target: Sprite | Stage): boolean { if (typeof target === "string") { console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Cannot find target "${target}" in "touchingColor". Did you mean to pass a sprite class instead?` ); return false; @@ -686,6 +793,7 @@ export class Sprite extends SpriteBase { if (typeof color === "string") { console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions `Cannot find color "${color}" in "touchingColor". Did you mean to pass a Color instance instead?` ); return false; @@ -693,10 +801,10 @@ export class Sprite extends SpriteBase { if (target instanceof Color) { // "Color is touching color" - return this._project.renderer.checkColorCollision(this, target, color); + return !!this._project?.renderer.checkColorCollision(this, target, color); } else { // "Color is touching sprite" (not implemented in Scratch!) - return this._project.renderer.checkSpriteCollision( + return !!this._project?.renderer.checkSpriteCollision( this, target, false, @@ -705,77 +813,88 @@ export class Sprite extends SpriteBase { } } - say(text) { - clearTimeout(this._speechBubble.timeout); + say(text: string) { + if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); this._speechBubble = { text: String(text), style: "say", timeout: null }; } - think(text) { - clearTimeout(this._speechBubble.timeout); + think(text: string) { + if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); this._speechBubble = { text: String(text), style: "think", timeout: null }; } - *sayAndWait(text, seconds) { - clearTimeout(this._speechBubble.timeout); + *sayAndWait(text: string, seconds: number) { + if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); + const speechBubble: SpeechBubble = { text, style: "say", timeout: null }; let done = false; - const timeout = setTimeout(() => { - this._speechBubble.text = ""; - this.timeout = null; + const timeout = window.setTimeout(() => { + speechBubble.text = ""; + speechBubble.timeout = null; done = true; }, seconds * 1000); - this._speechBubble = { text, style: "say", timeout }; + speechBubble.timeout = timeout; + this._speechBubble = speechBubble; while (!done) yield; } - *thinkAndWait(text, seconds) { - clearTimeout(this._speechBubble.timeout); + *thinkAndWait(text: string, seconds: number) { + if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); + const speechBubble: SpeechBubble = { text, style: "think", timeout: null }; let done = false; - const timeout = setTimeout(() => { - this._speechBubble.text = ""; - this.timeout = null; + const timeout = window.setTimeout(() => { + speechBubble.text = ""; + speechBubble.timeout = null; done = true; }, seconds * 1000); - this._speechBubble = { text, style: "think", timeout }; + speechBubble.timeout = timeout; + this._speechBubble = speechBubble; while (!done) yield; } + + static RotationStyle = Object.freeze({ + ALL_AROUND: Symbol("ALL_AROUND"), + LEFT_RIGHT: Symbol("LEFT_RIGHT"), + DONT_ROTATE: Symbol("DONT_ROTATE"), + }); } -Sprite.RotationStyle = Object.freeze({ - ALL_AROUND: Symbol("ALL_AROUND"), - LEFT_RIGHT: Symbol("LEFT_RIGHT"), - DONT_ROTATE: Symbol("DONT_ROTATE") -}); +type StageInitialConditions = { + width?: number; + height?: number; +} & InitialConditions; + +export class Stage extends SpriteBase { + readonly width!: number; + readonly height!: number; + __counter: number; -export class Stage extends SpriteBase { - constructor(initialConditions, ...args) { - super(initialConditions, ...args); + constructor(initialConditions: StageInitialConditions, vars: Vars) { + super(initialConditions, vars); // Use defineProperties to make these non-writable. // Changing the width and height of the stage after initialization isn't supported. Object.defineProperties(this, { width: { value: initialConditions.width || 480, - enumerable: true + enumerable: true, }, height: { value: initialConditions.height || 360, - enumerable: true - } + enumerable: true, + }, }); - this.name = "Stage"; - // For obsolete counter blocks. this.__counter = 0; } fireBackdropChanged() { - return this._project.fireTrigger(Trigger.BACKDROP_CHANGED, { - backdrop: this.costume.name + return this._project?.fireTrigger(Trigger.BACKDROP_CHANGED, { + backdrop: this.costume.name, }); } } diff --git a/src/Trigger.js b/src/Trigger.ts similarity index 63% rename from src/Trigger.js rename to src/Trigger.ts index fcfe512..3cb416b 100644 --- a/src/Trigger.js +++ b/src/Trigger.ts @@ -1,3 +1,5 @@ +import type { Sprite, Stage } from "./Sprite.js"; + const GREEN_FLAG = Symbol("GREEN_FLAG"); const KEY_PRESSED = Symbol("KEY_PRESSED"); const BROADCAST = Symbol("BROADCAST"); @@ -7,19 +9,39 @@ const LOUDNESS_GREATER_THAN = Symbol("LOUDNESS_GREATER_THAN"); const TIMER_GREATER_THAN = Symbol("TIMER_GREATER_THAN"); const BACKDROP_CHANGED = Symbol("BACKDROP_CHANGED"); +type TriggerOption = + | number + | string + | boolean + | ((target: Sprite | Stage) => number | string | boolean); + +type TriggerOptions = Partial>; + export default class Trigger { - constructor(trigger, options, script) { + trigger: symbol; + options: TriggerOptions; + _script: GeneratorFunction; + _runningScript: Generator | undefined; + done: boolean; + stop: () => void; + + constructor( + trigger: Trigger["trigger"], + options: Trigger["options"] | Trigger["_script"], + script?: Trigger["_script"] + ) { this.trigger = trigger; if (typeof script === "undefined") { this.options = {}; - this._script = options; + this._script = options as Trigger["_script"]; } else { - this.options = options; + this.options = options as Trigger["options"]; this._script = script; } this.done = false; + // eslint-disable-next-line @typescript-eslint/no-empty-function this.stop = () => {}; } @@ -32,8 +54,8 @@ export default class Trigger { // Evaluate the given trigger option, whether it's a value or a function that // returns a value given a target - option(option, target) { - let triggerOption = this.options[option]; + option(option: string, target: Sprite | Stage) { + const triggerOption = this.options[option]; // If the given option is a function, evaluate that function, passing in // the target that we're evaluating the trigger for if (typeof triggerOption === "function") { @@ -42,16 +64,20 @@ export default class Trigger { return triggerOption; } - matches(trigger, options, target) { + matches( + trigger: Trigger["trigger"], + options: Trigger["options"] | undefined, + target: Sprite | Stage + ) { if (this.trigger !== trigger) return false; - for (let option in options) { + for (const option in options) { if (this.option(option, target) !== options[option]) return false; } return true; } - start(target) { + start(target: Sprite | Stage) { this.stop(); const boundScript = this._script.bind(target); @@ -59,7 +85,7 @@ export default class Trigger { this.done = false; this._runningScript = boundScript(); - return new Promise(resolve => { + return new Promise((resolve) => { this.stop = () => { this.done = true; resolve(); @@ -68,7 +94,8 @@ export default class Trigger { } step() { - this.done = this._runningScript.next().done; + if (!this._runningScript) return; + this.done = !!this._runningScript.next().done; if (this.done) this.stop(); } @@ -97,3 +124,5 @@ export default class Trigger { return BACKDROP_CHANGED; } } + +export type { TriggerOption, TriggerOptions }; diff --git a/src/Watcher.js b/src/Watcher.ts similarity index 72% rename from src/Watcher.js rename to src/Watcher.ts index bb9d5f5..e11c7b4 100644 --- a/src/Watcher.js +++ b/src/Watcher.ts @@ -1,8 +1,53 @@ import Color from "./Color"; +type WatcherValue = + | string + | number + | boolean + | null + | undefined + | (string | number | boolean | null | undefined)[]; + +type WatcherOptions = { + value?: () => WatcherValue; + setValue?: (value: number) => void; + label: string; + style?: "normal" | "large" | "slider"; + visible?: boolean; + color?: Color; + step?: number; + x?: number; + y?: number; + width?: number; + height?: number; +}; + export default class Watcher { + value: () => WatcherValue; + setValue: (value: number) => void; + _previousValue: unknown | symbol; + color: Color; + _label!: string; + _x!: number; + _y!: number; + _width: number | undefined; + _height: number | undefined; + _min!: number; + _max!: number; + _step!: number; + _style!: "normal" | "large" | "slider"; + _visible!: boolean; + + _dom!: { + node: HTMLElement; + label: HTMLElement; + value: HTMLElement; + slider: HTMLInputElement; + }; + constructor({ value = () => "", + // eslint-disable-next-line @typescript-eslint/no-empty-function setValue = () => {}, label, style = "normal", @@ -14,8 +59,8 @@ export default class Watcher { x = -240, y = 180, width, - height - }) { + height, + }: WatcherOptions) { this.initializeDOM(); this.value = value; @@ -34,6 +79,9 @@ export default class Watcher { this.y = y; this.width = width; this.height = height; + this.min = 0; + this.max = 100; + this.step = 1; } initializeDOM() { @@ -52,8 +100,8 @@ export default class Watcher { slider.type = "range"; slider.classList.add("leopard__watcherSlider"); - slider.addEventListener("input", event => { - this.setValue(Number(event.target.value)); + slider.addEventListener("input", () => { + this.setValue(Number(slider.value)); }); node.append(slider); @@ -61,7 +109,7 @@ export default class Watcher { this._dom = { node, label, value, slider }; } - updateDOM(renderTarget) { + updateDOM(renderTarget: HTMLElement) { if (renderTarget && !renderTarget.contains(this._dom.node)) { renderTarget.append(this._dom.node); } @@ -86,11 +134,11 @@ export default class Watcher { const indexElem = document.createElement("div"); indexElem.classList.add("leopard__watcherListItemIndex"); - indexElem.innerText = index; + indexElem.innerText = String(index); const contentElem = document.createElement("div"); contentElem.classList.add("leopard__watcherListItemContent"); - contentElem.innerText = item.toString(); + contentElem.innerText = String(item); itemElem.append(indexElem); itemElem.append(contentElem); @@ -100,7 +148,7 @@ export default class Watcher { } else { // Render like a normal variable if (value !== this._previousValue) { - this._dom.value.innerText = value.toString(); + this._dom.value.innerText = String(value); } } @@ -112,7 +160,8 @@ export default class Watcher { // Set slider value if (this._style === "slider") { - this._dom.slider.value = value; + // TODO: handle non-numeric slider values + this._dom.slider.value = String(value); } // Update color @@ -158,7 +207,7 @@ export default class Watcher { if (width) { this._dom.node.style.width = `${width}px`; } else { - this._dom.node.style.width = undefined; + this._dom.node.style.removeProperty("width"); } } @@ -170,7 +219,7 @@ export default class Watcher { if (height) { this._dom.node.style.height = `${height}px`; } else { - this._dom.node.style.height = undefined; + this._dom.node.style.removeProperty("height"); } } @@ -193,20 +242,20 @@ export default class Watcher { ); } - get min() { + get min(): number { return this._min; } - set min(min) { + set min(min: number) { this._min = min; - this._dom.slider.min = min; + this._dom.slider.min = String(min); } - get max() { + get max(): number { return this._max; } - set max(max) { + set max(max: number) { this._max = max; - this._dom.slider.max = max; + this._dom.slider.max = String(max); } get step() { @@ -214,7 +263,7 @@ export default class Watcher { } set step(step) { this._step = step; - this._dom.slider.step = step; + this._dom.slider.step = String(step); } get label() { diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/lib/decode-adpcm-audio.js b/src/lib/decode-adpcm-audio.ts similarity index 88% rename from src/lib/decode-adpcm-audio.js rename to src/lib/decode-adpcm-audio.ts index c971d1a..1c43a37 100644 --- a/src/lib/decode-adpcm-audio.js +++ b/src/lib/decode-adpcm-audio.ts @@ -32,14 +32,17 @@ const ADPCM_STEPS = [ const ADPCM_INDEX = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8]; -export default function decodeADPCMAudio(ab, audioContext) { +export default function decodeADPCMAudio( + ab: ArrayBuffer, + audioContext: AudioContext +) { const dv = new DataView(ab); // WAV magic number if (dv.getUint32(0) !== 0x52494646 || dv.getUint32(8) !== 0x57415645) { return Promise.reject(new Error("Unrecognized audio format")); } - const blocks = {}; + const blocks: Partial> = {}; const l = dv.byteLength - 8; let i = 12; while (i < l) { @@ -54,6 +57,12 @@ export default function decodeADPCMAudio(ab, audioContext) { i += 8 + dv.getUint32(i + 4, true); } + const factBlock = blocks.fact; + const dataBlock = blocks.data; + if (typeof factBlock !== "number" || typeof dataBlock !== "number") { + return Promise.reject(new Error("Invalid WAV")); + } + const format = dv.getUint16(20, true); const sampleRate = dv.getUint32(24, true); @@ -61,17 +70,17 @@ export default function decodeADPCMAudio(ab, audioContext) { const samplesPerBlock = dv.getUint16(38, true); const blockSize = (samplesPerBlock - 1) / 2 + 4; - const frameCount = dv.getUint32(blocks.fact + 8, true); + const frameCount = dv.getUint32(factBlock + 8, true); const buffer = audioContext.createBuffer(1, frameCount, sampleRate); const channel = buffer.getChannelData(0); - let sample; + let sample = 0; let index = 0; let step, code, delta; let lastByte = -1; - const offset = blocks.data + 8; + const offset = dataBlock + 8; let i = offset; let j = 0; // eslint-disable-next-line @@ -115,14 +124,14 @@ export default function decodeADPCMAudio(ab, audioContext) { return Promise.reject(new Error(`Unrecognized WAV format ${format}`)); } -export function isWavData(arrayBuffer) { +export function isWavData(arrayBuffer: ArrayBuffer) { const dataView = new DataView(arrayBuffer); return ( dataView.getUint32(0) === 0x52494646 && dataView.getUint32(8) === 0x57415645 ); } -export function isADPCMData(arrayBuffer) { +export function isADPCMData(arrayBuffer: ArrayBuffer) { const dataView = new DataView(arrayBuffer); const format = dataView.getUint16(20, true); return isWavData(arrayBuffer) && format === 17; diff --git a/src/renderer/BitmapSkin.js b/src/renderer/BitmapSkin.ts similarity index 84% rename from src/renderer/BitmapSkin.js rename to src/renderer/BitmapSkin.ts index 399508e..7a14114 100644 --- a/src/renderer/BitmapSkin.js +++ b/src/renderer/BitmapSkin.ts @@ -1,7 +1,12 @@ +import type Renderer from "../Renderer.js"; import Skin from "./Skin.js"; export default class BitmapSkin extends Skin { - constructor(renderer, image) { + _image: HTMLImageElement; + _imageData: ImageData | null; + _texture: WebGLTexture | null; + + constructor(renderer: Renderer, image: HTMLImageElement) { super(renderer); this._image = image; @@ -20,6 +25,7 @@ export default class BitmapSkin extends Skin { canvas.width = this._image.naturalWidth || this._image.width; canvas.height = this._image.naturalHeight || this._image.height; const ctx = canvas.getContext("2d"); + if (!ctx) return null; ctx.drawImage(this._image, 0, 0); // Cache image data so we can reuse it this._imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); diff --git a/src/renderer/Drawable.js b/src/renderer/Drawable.ts similarity index 81% rename from src/renderer/Drawable.js rename to src/renderer/Drawable.ts index d2a7819..bb0b8ad 100644 --- a/src/renderer/Drawable.js +++ b/src/renderer/Drawable.ts @@ -1,15 +1,20 @@ -import Matrix from "./Matrix.js"; +import Matrix, { MatrixType } from "./Matrix.js"; import Rectangle from "./Rectangle.js"; import effectTransformPoint from "./effectTransformPoint.js"; import { effectBitmasks } from "./effectInfo.js"; +import type Renderer from "../Renderer.js"; import { Sprite, Stage } from "../Sprite.js"; // Returns the determinant of two vectors, the vector from A to B and the vector // from A to C. If positive, it means AC is counterclockwise from AB. // If negative, AC is clockwise from AB. -const determinant = (a, b, c) => { +const determinant = ( + a: [number, number], + b: [number, number], + c: [number, number] +) => { return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); }; @@ -18,18 +23,31 @@ const determinant = (a, b, c) => { // TODO: store renderer-specific data on the sprite and have *it* set a // "transform changed" flag. class SpriteTransformDiff { - constructor(sprite) { + _sprite: Sprite | Stage; + _unset: boolean; + + _lastX!: Sprite["x"] | undefined; + _lastY!: Sprite["y"] | undefined; + _lastRotation!: Sprite["direction"] | undefined; + _lastRotationStyle!: Sprite["rotationStyle"] | undefined; + _lastSize!: Sprite["size"] | undefined; + _lastCostume!: Sprite["costume"]; + _lastCostumeLoaded!: boolean; + + constructor(sprite: Sprite | Stage) { this._sprite = sprite; this._unset = true; this.update(); } update() { - this._lastX = this._sprite.x; - this._lastY = this._sprite.y; - this._lastRotation = this._sprite.direction; - this._lastRotationStyle = this._sprite.rotationStyle; - this._lastSize = this._sprite.size; + if (this._sprite instanceof Sprite) { + this._lastX = this._sprite.x; + this._lastY = this._sprite.y; + this._lastRotation = this._sprite.direction; + this._lastRotationStyle = this._sprite.rotationStyle; + this._lastSize = this._sprite.size; + } this._lastCostume = this._sprite.costume; this._lastCostumeLoaded = this._sprite.costume.img.complete; this._unset = false; @@ -37,11 +55,12 @@ class SpriteTransformDiff { get changed() { return ( - this._lastX !== this._sprite.x || - this._lastY !== this._sprite.y || - this._lastRotation !== this._sprite.direction || - this._lastRotationStyle !== this._sprite.rotationStyle || - this._lastSize !== this._sprite.size || + (this._sprite instanceof Sprite && + (this._lastX !== this._sprite.x || + this._lastY !== this._sprite.y || + this._lastRotation !== this._sprite.direction || + this._lastRotationStyle !== this._sprite.rotationStyle || + this._lastSize !== this._sprite.size)) || this._lastCostume !== this._sprite.costume || this._lastCostumeLoaded !== this._sprite.costume.img.complete || this._unset @@ -51,7 +70,23 @@ class SpriteTransformDiff { // Renderer-specific data for an instance (the original or a clone) of a Sprite export default class Drawable { - constructor(renderer, sprite) { + _renderer: Renderer; + _sprite: Sprite | Stage; + _matrix: MatrixType; + _matrixDiff: SpriteTransformDiff; + + _convexHullImageData: ImageData | null; + _convexHullMosaic: number; + _convexHullPixelate: number; + _convexHullWhirl: number; + _convexHullFisheye: number; + _convexHullPoints: [number, number][] | null; + + _aabb: Rectangle; + _tightBoundingBox: Rectangle; + _convexHullMatrixDiff: SpriteTransformDiff; + + constructor(renderer: Renderer, sprite: Sprite | Stage) { this._renderer = renderer; this._sprite = sprite; @@ -97,12 +132,20 @@ export default class Drawable { const convexHullPoints = this._calculateConvexHull(); // Maybe the costume isn't loaded yet. Return a 0x0 bounding box around the // center of the sprite. - if (convexHullPoints === null) { + if (convexHullPoints === null || this._convexHullImageData === null) { + if (this._sprite instanceof Stage) { + return Rectangle.fromBounds( + this._sprite.width / -2, + this._sprite.width / 2, + this._sprite.height / -2, + this._sprite.height / 2 + ); + } return Rectangle.fromBounds( this._sprite.x, - this._sprite.y, this._sprite.x, this._sprite.y, + this._sprite.y, this._tightBoundingBox ); } @@ -111,7 +154,7 @@ export default class Drawable { let right = -Infinity; let top = -Infinity; let bottom = Infinity; - const transformedPoint = [0, 0]; + const transformedPoint: [number, number] = [0, 0]; // Each convex hull point is the center of a pixel. However, said pixels // each have area. We must take into account the size of the pixels when @@ -175,14 +218,14 @@ export default class Drawable { effectBitmasks.whirl | effectBitmasks.fisheye); - const leftHull = []; - const rightHull = []; + const leftHull: [number, number][] = []; + const rightHull: [number, number][] = []; const { width, height, data } = imageData; - const pixelPos = [0, 0]; - const effectPos = [0, 0]; - let currentPoint; + const pixelPos: [number, number] = [0, 0]; + const effectPos: [number, number] = [0, 0]; + let currentPoint: [number, number] | undefined; // Not Scratch-space: y increases as we go downwards // Loop over all rows of pixels in the costume, starting at the top for (let y = 0; y < height; y++) { @@ -208,7 +251,7 @@ export default class Drawable { } // There are no opaque pixels on this row. Go to the next one. - if (x >= width) continue; + if (x >= width || !currentPoint) continue; // If appending the current point to the left hull makes a // counterclockwise turn, we want to append the current point to it. diff --git a/src/renderer/Matrix.js b/src/renderer/Matrix.ts similarity index 83% rename from src/renderer/Matrix.js rename to src/renderer/Matrix.ts index 44fae75..a699ccd 100644 --- a/src/renderer/Matrix.js +++ b/src/renderer/Matrix.ts @@ -12,7 +12,7 @@ export default class Matrix { } // Reset a matrix to the identity matrix - static identity(dst) { + static identity(dst: MatrixType) { dst[0] = 1; dst[1] = 0; dst[2] = 0; @@ -26,7 +26,7 @@ export default class Matrix { } // Translate a matrix by the given X and Y values - static translate(dst, src, x, y) { + static translate(dst: MatrixType, src: MatrixType, x: number, y: number) { const a00 = src[0], a01 = src[1], a02 = src[2], @@ -52,7 +52,7 @@ export default class Matrix { } // Rotate a matrix, in radians - static rotate(dst, src, rad) { + static rotate(dst: MatrixType, src: MatrixType, rad: number) { const a00 = src[0], a01 = src[1], a02 = src[2], @@ -80,7 +80,7 @@ export default class Matrix { } // Scale a matrix by the given X and Y values - static scale(dst, src, x, y) { + static scale(dst: MatrixType, src: MatrixType, x: number, y: number) { dst[0] = x * src[0]; dst[1] = x * src[1]; dst[2] = x * src[2]; @@ -96,7 +96,11 @@ export default class Matrix { } // Transform a 2D point by the given matrix - static transformPoint(m, dst, src) { + static transformPoint( + m: MatrixType, + dst: [number, number], + src: [number, number] + ) { const x = src[0]; const y = src[1]; dst[0] = m[0] * x + m[3] * y + m[6]; @@ -104,3 +108,5 @@ export default class Matrix { return dst; } } + +export type MatrixType = Float32Array; diff --git a/src/renderer/PenSkin.js b/src/renderer/PenSkin.ts similarity index 87% rename from src/renderer/PenSkin.js rename to src/renderer/PenSkin.ts index 542c817..4a8e2ce 100644 --- a/src/renderer/PenSkin.js +++ b/src/renderer/PenSkin.ts @@ -1,8 +1,17 @@ import Skin from "./Skin.js"; import ShaderManager from "./ShaderManager.js"; +import type Color from "../Color.js"; +import type Renderer from "../Renderer.js"; +import type { FramebufferInfo } from "../Renderer.js"; export default class PenSkin extends Skin { - constructor(renderer, width, height) { + _framebufferInfo: FramebufferInfo; + _lastPenState: { + size: number; + color: [number, number, number, number]; + }; + + constructor(renderer: Renderer, width: number, height: number) { super(renderer); this.width = width; this.height = height; @@ -16,7 +25,7 @@ export default class PenSkin extends Skin { this._lastPenState = { size: 0, - color: [0, 0, 0, 0] + color: [0, 0, 0, 0], }; this.clear(); @@ -32,7 +41,12 @@ export default class PenSkin extends Skin { return this._framebufferInfo.texture; } - penLine(pt1, pt2, color, size) { + penLine( + pt1: { x: number; y: number }, + pt2: { x: number; y: number }, + color: Color, + size: number + ) { const renderer = this.renderer; renderer._setFramebuffer(this._framebufferInfo); diff --git a/src/renderer/Rectangle.js b/src/renderer/Rectangle.ts similarity index 80% rename from src/renderer/Rectangle.js rename to src/renderer/Rectangle.ts index 149bfa4..7096d37 100644 --- a/src/renderer/Rectangle.js +++ b/src/renderer/Rectangle.ts @@ -1,4 +1,11 @@ +import type { MatrixType } from "./Matrix.js"; + export default class Rectangle { + left: number; + right: number; + bottom: number; + top: number; + constructor() { this.left = -Infinity; this.right = Infinity; @@ -8,8 +15,13 @@ export default class Rectangle { return this; } - static fromBounds(left, right, bottom, top, result) { - if (!result) result = new Rectangle(); + static fromBounds( + left: number, + right: number, + bottom: number, + top: number, + result = new Rectangle() + ) { result.left = left; result.right = right; result.bottom = bottom; @@ -19,9 +31,7 @@ export default class Rectangle { } // Initialize a bounding box around a sprite given the sprite's transform matrix. - static fromMatrix(matrix, result) { - if (!result) result = new Rectangle(); - + static fromMatrix(matrix: MatrixType, result = new Rectangle()) { // Adapted somewhat from https://github.com/LLK/scratch-render/blob/develop/docs/Rectangle-AABB-Matrix.md const xa = matrix[0] / 2; const xb = matrix[3] / 2; @@ -42,7 +52,7 @@ export default class Rectangle { } // Initialize from another rectangle. - static copy(src, dst) { + static copy(src: Rectangle, dst: Rectangle) { dst.left = src.left; dst.right = src.right; dst.bottom = src.bottom; @@ -62,7 +72,7 @@ export default class Rectangle { } // Check whether any part of this rectangle touches another rectangle. - intersects(rect) { + intersects(rect: Rectangle) { return ( this.left <= rect.right && rect.left <= this.right && @@ -72,14 +82,14 @@ export default class Rectangle { } // Check whether a given point is inside this rectangle. - containsPoint(x, y) { + containsPoint(x: number, y: number) { return ( x >= this.left && x <= this.right && y >= this.bottom && y <= this.top ); } // Clamp this rectangle within bounds. - clamp(left, right, bottom, top) { + clamp(left: number, right: number, bottom: number, top: number) { this.left = Math.min(Math.max(this.left, left), right); this.right = Math.max(Math.min(this.right, right), left); this.bottom = Math.min(Math.max(this.bottom, bottom), top); @@ -89,7 +99,7 @@ export default class Rectangle { } // Compute the union of two rectangles. - static union(rect1, rect2, result = new Rectangle()) { + static union(rect1: Rectangle, rect2: Rectangle, result = new Rectangle()) { result.left = Math.min(rect1.left, rect2.left); result.right = Math.max(rect1.right, rect2.right); result.bottom = Math.min(rect1.bottom, rect2.bottom); @@ -99,7 +109,11 @@ export default class Rectangle { } // Compute the intersection of two rectangles. - static intersection(rect1, rect2, result = new Rectangle()) { + static intersection( + rect1: Rectangle, + rect2: Rectangle, + result = new Rectangle() + ) { result.left = Math.max(rect1.left, rect2.left); result.right = Math.min(rect1.right, rect2.right); result.bottom = Math.max(rect1.bottom, rect2.bottom); diff --git a/src/renderer/ShaderManager.js b/src/renderer/ShaderManager.js deleted file mode 100644 index 4118d2d..0000000 --- a/src/renderer/ShaderManager.js +++ /dev/null @@ -1,135 +0,0 @@ -import { SpriteShader, PenLineShader } from "./Shaders.js"; -import { effectNames, effectBitmasks } from "./effectInfo.js"; - -// Everything contained in a shader. It contains both the program, and the locations of the shader inputs. -class Shader { - constructor(gl, program) { - this.gl = gl; - this.program = program; - this.uniforms = {}; - this.attribs = {}; - - // In order to pass a value into a shader as an attribute or uniform, you need to know its location. - // This maps the names of attributes and uniforms to their locations, accessible via the `uniforms` and `attribs` - // properties. - const numActiveUniforms = gl.getProgramParameter( - program, - gl.ACTIVE_UNIFORMS - ); - for (let i = 0; i < numActiveUniforms; i++) { - const { name } = gl.getActiveUniform(program, i); - this.uniforms[name] = gl.getUniformLocation(program, name); - } - - const numActiveAttributes = gl.getProgramParameter( - program, - gl.ACTIVE_ATTRIBUTES - ); - for (let i = 0; i < numActiveAttributes; i++) { - const { name } = gl.getActiveAttrib(program, i); - this.attribs[name] = gl.getAttribLocation(program, name); - } - } -} - -class ShaderManager { - constructor(renderer) { - this.renderer = renderer; - this.gl = renderer.gl; - - // We compile shaders on-demand. Create one shader cache per draw mode. - this._shaderCache = {}; - for (const drawMode of Object.keys(ShaderManager.DrawModes)) { - this._shaderCache[drawMode] = new Map(); - } - } - - // Creates and compiles a vertex or fragment shader from the given source code. - _createShader(source, type) { - const gl = this.gl; - const shader = gl.createShader(type); - gl.shaderSource(shader, source); - gl.compileShader(shader); - - if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { - const info = gl.getShaderInfoLog(shader); - throw "Could not compile WebGL program. \n" + info; - } - - return shader; - } - - getShader(drawMode, effectBitmask = 0) { - const gl = this.gl; - // Each combination of enabled effects is compiled to a different shader, with only the needed effect code. - // Check if we've already compiled the shader with this set of enabled effects. - const shaderMap = this._shaderCache[drawMode]; - if (shaderMap.has(effectBitmask)) { - return shaderMap.get(effectBitmask); - } else { - let shaderCode; - switch (drawMode) { - case ShaderManager.DrawModes.PEN_LINE: { - shaderCode = PenLineShader; - break; - } - default: { - shaderCode = SpriteShader; - break; - } - } - - // Use #define statements for conditional compilation in shader code. - let define = `#define DRAW_MODE_${drawMode}\n`; - - // Add #defines for each enabled effect. - for (let i = 0; i < effectNames.length; i++) { - const effectName = effectNames[i]; - if ((effectBitmask & effectBitmasks[effectName]) !== 0) { - define += `#define EFFECT_${effectName}\n`; - } - } - - const vertShader = this._createShader( - define + shaderCode.vertex, - gl.VERTEX_SHADER - ); - const fragShader = this._createShader( - define + shaderCode.fragment, - gl.FRAGMENT_SHADER - ); - - // Combine the vertex and fragment shaders into a single GL program. - const program = gl.createProgram(); - gl.attachShader(program, vertShader); - gl.attachShader(program, fragShader); - gl.linkProgram(program); - - if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { - const info = gl.getProgramInfoLog(program); - throw new Error("Could not compile WebGL program. \n" + info); - } - - const shader = new Shader(gl, program); - shaderMap.set(effectBitmask, shader); - return shader; - } - } -} - -ShaderManager.DrawModes = { - // Used for drawing sprites normally - DEFAULT: "DEFAULT", - // Used for "touching" tests. Discards transparent pixels. - SILHOUETTE: "SILHOUETTE", - // Used for "color is touching color" tests. Only renders sprite colors which are close to the color passed in, and - // discards all pixels of a different color. - COLOR_MASK: "COLOR_MASK", - // Used for picking the topmost sprite and identifying which one it is. - // Assigns a color to each sprite. - SPRITE_ID: "SPRITE_ID", - // Used for drawing pen lines. - PEN_LINE: "PEN_LINE" -}; - -export default ShaderManager; diff --git a/src/renderer/ShaderManager.ts b/src/renderer/ShaderManager.ts new file mode 100644 index 0000000..965c0aa --- /dev/null +++ b/src/renderer/ShaderManager.ts @@ -0,0 +1,157 @@ +import { SpriteShader, PenLineShader } from "./Shaders.js"; +import { effectNames, effectBitmasks } from "./effectInfo.js"; +import type Renderer from "../Renderer.js"; + +// Everything contained in a shader. It contains both the program, and the locations of the shader inputs. +class Shader { + gl: WebGLRenderingContext; + program: WebGLProgram; + // TODO: strongly type these + uniforms: Record; + attribs: Record; + + constructor(gl: WebGLRenderingContext, program: WebGLProgram) { + this.gl = gl; + this.program = program; + this.uniforms = {}; + this.attribs = {}; + + // In order to pass a value into a shader as an attribute or uniform, you need to know its location. + // This maps the names of attributes and uniforms to their locations, accessible via the `uniforms` and `attribs` + // properties. + const numActiveUniforms = gl.getProgramParameter( + program, + gl.ACTIVE_UNIFORMS + ) as number; + for (let i = 0; i < numActiveUniforms; i++) { + const { name } = gl.getActiveUniform(program, i)!; + this.uniforms[name] = gl.getUniformLocation(program, name)!; + } + + const numActiveAttributes = gl.getProgramParameter( + program, + gl.ACTIVE_ATTRIBUTES + ) as number; + for (let i = 0; i < numActiveAttributes; i++) { + const { name } = gl.getActiveAttrib(program, i)!; + this.attribs[name] = gl.getAttribLocation(program, name)!; + } + } +} + +type DrawMode = keyof typeof ShaderManager["DrawModes"]; + +class ShaderManager { + renderer: Renderer; + gl: WebGLRenderingContext; + + _shaderCache: Record>; + + constructor(renderer: Renderer) { + this.renderer = renderer; + this.gl = renderer.gl; + + // We compile shaders on-demand. Create one shader cache per draw mode. + this._shaderCache = {} as Record>; + for (const drawMode of Object.keys(ShaderManager.DrawModes)) { + this._shaderCache[drawMode as DrawMode] = new Map(); + } + } + + // Creates and compiles a vertex or fragment shader from the given source code. + _createShader( + source: string, + type: + | WebGLRenderingContext["FRAGMENT_SHADER"] + | WebGLRenderingContext["VERTEX_SHADER"] + ) { + const gl = this.gl; + const shader = gl.createShader(type); + if (!shader) throw new Error("Could not create shader."); + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + const info = gl.getShaderInfoLog(shader) ?? ""; + throw new Error("Could not compile WebGL program. \n" + info); + } + + return shader; + } + + getShader(drawMode: DrawMode, effectBitmask = 0): Shader { + const gl = this.gl; + // Each combination of enabled effects is compiled to a different shader, with only the needed effect code. + // Check if we've already compiled the shader with this set of enabled effects. + const shaderMap = this._shaderCache[drawMode]; + const existingShader = shaderMap.get(effectBitmask); + if (existingShader) return existingShader; + + let shaderCode; + switch (drawMode) { + case ShaderManager.DrawModes.PEN_LINE: { + shaderCode = PenLineShader; + break; + } + default: { + shaderCode = SpriteShader; + break; + } + } + + // Use #define statements for conditional compilation in shader code. + let define = `#define DRAW_MODE_${drawMode}\n`; + + // Add #defines for each enabled effect. + for (let i = 0; i < effectNames.length; i++) { + const effectName = effectNames[i]; + if ((effectBitmask & effectBitmasks[effectName]) !== 0) { + define += `#define EFFECT_${effectName}\n`; + } + } + + const vertShader = this._createShader( + define + shaderCode.vertex, + gl.VERTEX_SHADER + ); + const fragShader = this._createShader( + define + shaderCode.fragment, + gl.FRAGMENT_SHADER + ); + + // Combine the vertex and fragment shaders into a single GL program. + const program = gl.createProgram(); + if (!program) throw new Error("Could not create program"); + gl.attachShader(program, vertShader); + gl.attachShader(program, fragShader); + gl.linkProgram(program); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + const info = gl.getProgramInfoLog(program) ?? ""; + throw new Error("Could not compile WebGL program. \n" + info); + } + + const shader = new Shader(gl, program); + shaderMap.set(effectBitmask, shader); + return shader; + } + + static DrawModes = { + // Used for drawing sprites normally + DEFAULT: "DEFAULT", + // Used for "touching" tests. Discards transparent pixels. + SILHOUETTE: "SILHOUETTE", + // Used for "color is touching color" tests. Only renders sprite colors which are close to the color passed in, and + // discards all pixels of a different color. + COLOR_MASK: "COLOR_MASK", + // Used for picking the topmost sprite and identifying which one it is. + // Assigns a color to each sprite. + SPRITE_ID: "SPRITE_ID", + // Used for drawing pen lines. + PEN_LINE: "PEN_LINE", + } as const; +} + +export default ShaderManager; +export { Shader }; +export type { DrawMode }; diff --git a/src/renderer/Shaders.js b/src/renderer/Shaders.ts similarity index 96% rename from src/renderer/Shaders.js rename to src/renderer/Shaders.ts index b443ec6..2dceae7 100644 --- a/src/renderer/Shaders.js +++ b/src/renderer/Shaders.ts @@ -1,6 +1,4 @@ -const SpriteShader = {}; - -SpriteShader.vertex = ` +const spriteShaderVertex = ` precision mediump float; attribute vec2 a_position; @@ -15,7 +13,7 @@ void main() { } `; -SpriteShader.fragment = ` +const spriteShaderFragment = ` precision mediump float; const float epsilon = 1e-3; @@ -184,10 +182,12 @@ void main() { gl_FragColor = color; } `; +const SpriteShader = { + vertex: spriteShaderVertex, + fragment: spriteShaderFragment, +}; -const PenLineShader = {}; - -PenLineShader.vertex = ` +const penLineShaderVertex = ` precision mediump float; attribute vec2 a_position; @@ -249,7 +249,7 @@ void main() { } `; -PenLineShader.fragment = ` +const penLineShaderFragment = ` precision mediump float; uniform sampler2D u_texture; @@ -280,4 +280,9 @@ void main() { } `; +const PenLineShader = { + vertex: penLineShaderVertex, + fragment: penLineShaderFragment, +}; + export { SpriteShader, PenLineShader }; diff --git a/src/renderer/Skin.js b/src/renderer/Skin.ts similarity index 67% rename from src/renderer/Skin.js rename to src/renderer/Skin.ts index dc709f6..4d23179 100644 --- a/src/renderer/Skin.js +++ b/src/renderer/Skin.ts @@ -1,25 +1,38 @@ -export default class Skin { - constructor(renderer) { +import type Renderer from "../Renderer.js"; + +export default abstract class Skin { + renderer: Renderer; + gl: WebGLRenderingContext; + width: number; + height: number; + + constructor(renderer: Renderer) { this.renderer = renderer; this.gl = renderer.gl; + this.width = 0; + this.height = 0; } // Get the skin's texture for a given (screen-space) scale. - /* eslint-disable-next-line no-unused-vars */ - getTexture(scale) { - return null; - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + abstract getTexture(scale: number): WebGLTexture | null; // Get the skin image's ImageData at a given (screen-space) scale. - // eslint-disable-next-line no-unused-vars - getImageData(scale) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getImageData(scale: number): ImageData | null { throw new Error("getImageData not implemented for this skin type"); } // Helper function to create a texture from an image and handle all the boilerplate. - _makeTexture(image, filtering) { + _makeTexture( + image: HTMLImageElement | HTMLCanvasElement | null, + filtering: + | WebGLRenderingContext["NEAREST"] + | WebGLRenderingContext["LINEAR"] + ): WebGLTexture { const gl = this.gl; const glTexture = gl.createTexture(); + if (!glTexture) throw new Error("Could not create texture"); gl.bindTexture(gl.TEXTURE_2D, glTexture); // These need to be set because most sprite textures don't have power-of-two dimensions. // Non-power-of-two textures only work with gl.CLAMP_TO_EDGE wrapping behavior, @@ -42,7 +55,7 @@ export default class Skin { } // Helper function to set this skin's size based on an image that may or may not be loaded. - _setSizeFromImage(image) { + _setSizeFromImage(image: HTMLImageElement) { if (image.complete) { this.width = image.naturalWidth; this.height = image.naturalHeight; @@ -55,5 +68,5 @@ export default class Skin { } // Clean up any textures or other objets created by this skin. - destroy() {} + abstract destroy(): void; } diff --git a/src/renderer/SpeechBubbleSkin.js b/src/renderer/SpeechBubbleSkin.ts similarity index 80% rename from src/renderer/SpeechBubbleSkin.js rename to src/renderer/SpeechBubbleSkin.ts index c5878cf..e3a6c56 100644 --- a/src/renderer/SpeechBubbleSkin.js +++ b/src/renderer/SpeechBubbleSkin.ts @@ -1,19 +1,34 @@ import Skin from "./Skin.js"; +import type Renderer from "../Renderer.js"; +import type { SpeechBubble } from "../Sprite.js"; const bubbleStyle = { maxLineWidth: 170, minWidth: 50, strokeWidth: 4, padding: 12, - tailHeight: 12 -}; + tailHeight: 12, +} as const; // TODO: multiline speech bubbles export default class SpeechBubbleSkin extends Skin { - constructor(renderer, bubble) { + _canvas: HTMLCanvasElement; + _ctx: CanvasRenderingContext2D; + _texture: WebGLTexture; + _bubble: SpeechBubble; + _flipped: boolean; + _rendered: boolean; + _renderedScale: number; + offsetX: number; + offsetY: number; + + constructor(renderer: Renderer, bubble: SpeechBubble) { super(renderer); this._canvas = document.createElement("canvas"); + const ctx = this._canvas.getContext("2d"); + if (ctx === null) throw new Error("Could not get canvas context"); + this._ctx = ctx; this._texture = this._makeTexture(null, this.gl.LINEAR); this._bubble = bubble; this._flipped = false; @@ -24,27 +39,32 @@ export default class SpeechBubbleSkin extends Skin { this.height = 0; this.offsetX = -bubbleStyle.strokeWidth / 2; this.offsetY = this.offsetX + bubbleStyle.tailHeight; - - this._renderBubble(this._bubble); } // To ensure proper text measurement and drawing, it's necessary to restyle the canvas after resizing it. _restyleCanvas() { - const ctx = this._canvas.getContext("2d"); + const ctx = this._ctx; ctx.font = "16px sans-serif"; ctx.textBaseline = "hanging"; } - set flipped(flipped) { + set flipped(flipped: boolean) { this._flipped = flipped; this._rendered = false; } - _renderBubble(bubble, scale) { + _renderBubble(bubble: SpeechBubble, scale: number) { const canvas = this._canvas; - const ctx = canvas.getContext("2d"); - - const renderBubbleBackground = (x, y, w, h, r, style) => { + const ctx = this._ctx; + + const renderBubbleBackground = ( + x: number, + y: number, + w: number, + h: number, + r: number, + style: "say" | "think" + ) => { if (r > w / 2) r = w / 2; if (r > h / 2) r = h / 2; if (r < 0) return; @@ -124,7 +144,7 @@ export default class SpeechBubbleSkin extends Skin { this._renderedScale = scale; } - getTexture(scale) { + getTexture(scale: number) { if (!this._rendered || this._renderedScale !== scale) { this._renderBubble(this._bubble, scale); const gl = this.gl; diff --git a/src/renderer/VectorSkin.js b/src/renderer/VectorSkin.ts similarity index 73% rename from src/renderer/VectorSkin.js rename to src/renderer/VectorSkin.ts index 21210ef..7169748 100644 --- a/src/renderer/VectorSkin.js +++ b/src/renderer/VectorSkin.ts @@ -1,51 +1,62 @@ import Skin from "./Skin.js"; +import type Renderer from "../Renderer.js"; // This means that the smallest mipmap will be 1/(2**4)th the size of the sprite's "100%" size. const MIPMAP_OFFSET = 4; export default class VectorSkin extends Skin { - constructor(renderer, image) { + _image: HTMLImageElement; + _canvas: HTMLCanvasElement; + _ctx: CanvasRenderingContext2D; + _imageDataMipLevel: number; + _imageData: ImageData | null; + _maxTextureSize: number; + _mipmaps: Map; + + constructor(renderer: Renderer, image: HTMLImageElement) { super(renderer); this._image = image; this._canvas = document.createElement("canvas"); + const ctx = this._canvas.getContext("2d"); + if (!ctx) throw new Error("Could not get canvas context"); + this._ctx = ctx; this._imageDataMipLevel = 0; this._imageData = null; this._maxTextureSize = renderer.gl.getParameter( renderer.gl.MAX_TEXTURE_SIZE - ); + ) as number; this._setSizeFromImage(image); this._mipmaps = new Map(); } - static mipLevelForScale(scale) { + static mipLevelForScale(scale: number) { return Math.max(Math.ceil(Math.log2(scale)) + MIPMAP_OFFSET, 0); } - getImageData(scale) { + getImageData(scale: number) { if (!this._image.complete) return null; // Round off the scale of the image data drawn to a given power-of-two mip level. const mipLevel = VectorSkin.mipLevelForScale(scale); if (!this._imageData || this._imageDataMipLevel !== mipLevel) { - const canvas = this._drawSvgToCanvas(mipLevel); - if (canvas === null) return null; + const ctx = this._drawSvgToCanvas(mipLevel); + if (ctx === null) return null; + const { canvas } = ctx; // Cache image data so we can reuse it - this._imageData = canvas - .getContext("2d") - .getImageData(0, 0, canvas.width, canvas.height); + this._imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); this._imageDataMipLevel = mipLevel; } return this._imageData; } - _drawSvgToCanvas(mipLevel) { + _drawSvgToCanvas(mipLevel: number): CanvasRenderingContext2D | null { const scale = 2 ** (mipLevel - MIPMAP_OFFSET); const image = this._image; @@ -61,30 +72,30 @@ export default class VectorSkin extends Skin { } // Instead of uploading the image to WebGL as a texture, render the image to a canvas and upload the canvas. - const canvas = this._canvas; - const ctx = canvas.getContext("2d"); + const ctx = this._ctx; + const { canvas } = ctx; canvas.width = width; canvas.height = height; ctx.drawImage(image, 0, 0, width, height); - return this._canvas; + return ctx; } // TODO: handle proper subpixel positioning when SVG viewbox has non-integer coordinates // This will require rethinking costume + project loading probably - _createMipmap(mipLevel) { + _createMipmap(mipLevel: number) { // Instead of uploading the image to WebGL as a texture, render the image to a canvas and upload the canvas. - const canvas = this._drawSvgToCanvas(mipLevel); + const ctx = this._drawSvgToCanvas(mipLevel); this._mipmaps.set( mipLevel, // Use linear (i.e. smooth) texture filtering for vectors // If the image is 0x0, we return null. Check for that. - canvas === null ? null : this._makeTexture(canvas, this.gl.LINEAR) + ctx === null ? null : this._makeTexture(ctx.canvas, this.gl.LINEAR) ); } - getTexture(scale) { + getTexture(scale: number) { if (!this._image.complete) return null; // Because WebGL doesn't support vector graphics, substitute a bunch of bitmaps. @@ -97,7 +108,7 @@ export default class VectorSkin extends Skin { const mipLevel = VectorSkin.mipLevelForScale(scale); if (!this._mipmaps.has(mipLevel)) this._createMipmap(mipLevel); - return this._mipmaps.get(mipLevel); + return this._mipmaps.get(mipLevel) ?? null; } destroy() { diff --git a/src/renderer/effectInfo.js b/src/renderer/effectInfo.ts similarity index 61% rename from src/renderer/effectInfo.js rename to src/renderer/effectInfo.ts index fe14cb8..e8346b2 100644 --- a/src/renderer/effectInfo.js +++ b/src/renderer/effectInfo.ts @@ -6,12 +6,17 @@ const effectNames = [ "pixelate", "mosaic", "brightness", - "ghost" -]; + "ghost", +] as const; -const effectBitmasks = {}; -for (let i = 0; i < effectNames.length; i++) { - effectBitmasks[effectNames[i]] = 1 << i; -} +const effectBitmasks = { + color: 1, + fisheye: 2, + whirl: 4, + pixelate: 8, + mosaic: 16, + brightness: 32, + ghost: 64, +} as const; export { effectNames, effectBitmasks }; diff --git a/src/renderer/effectTransformPoint.js b/src/renderer/effectTransformPoint.ts similarity index 94% rename from src/renderer/effectTransformPoint.js rename to src/renderer/effectTransformPoint.ts index bb1590d..942d797 100644 --- a/src/renderer/effectTransformPoint.js +++ b/src/renderer/effectTransformPoint.ts @@ -1,10 +1,15 @@ import { effectBitmasks } from "./effectInfo.js"; +import type Drawable from "./Drawable.js"; const CENTER = 0.5; const EPSILON = 1e-3; // Transform a texture-space point using the effects defined on the given drawable. -const effectTransformPoint = (drawable, src, dst) => { +const effectTransformPoint = ( + drawable: Drawable, + src: [number, number], + dst: [number, number] +): [number, number] => { const { effects } = drawable._sprite; const effectBitmask = effects._bitmask; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d530d53 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2020", + + "esModuleInterop": true, + "moduleResolution": "node", + "sourceMap": true, + "strict": true, + "allowJs": true, + + "outDir": "dist", + "declaration": true, + "declarationDir": "dist" + }, + "include": [ + "src/**/*" + ], + "exclude": ["dist"] +} From 1386a50d29ac8aa5ea676610982525be507cec1b Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 02/46] Specify function return types This will ensure we don't accidentally change the API surface by changing the functions' implementations in a way that causes TypeScript to infer a different return type. --- .eslintrc.json | 4 +- src/Color.ts | 48 +++++++----- src/Costume.ts | 4 +- src/Input.ts | 22 +++--- src/Loudness.ts | 8 +- src/Project.ts | 42 +++++------ src/Renderer.ts | 47 ++++++------ src/Sound.ts | 57 +++++++------- src/Sprite.ts | 123 +++++++++++++++++-------------- src/Trigger.ts | 47 +++++------- src/Watcher.ts | 26 ++++--- src/lib/decode-adpcm-audio.ts | 6 +- src/lib/yielding.ts | 7 ++ src/renderer/BitmapSkin.ts | 6 +- src/renderer/Drawable.ts | 19 ++--- src/renderer/Matrix.ts | 22 ++++-- src/renderer/PenSkin.ts | 8 +- src/renderer/Rectangle.ts | 26 ++++--- src/renderer/ShaderManager.ts | 2 +- src/renderer/Skin.ts | 2 +- src/renderer/SpeechBubbleSkin.ts | 10 +-- src/renderer/VectorSkin.ts | 10 +-- 22 files changed, 296 insertions(+), 250 deletions(-) create mode 100644 src/lib/yielding.ts diff --git a/.eslintrc.json b/.eslintrc.json index a256112..e22b24b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -30,7 +30,9 @@ "browser": true }, "rules": { - "@typescript-eslint/no-non-null-assertion": "off" + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/prefer-return-this-type": "error", + "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}] } } ] diff --git a/src/Color.ts b/src/Color.ts index b724d0d..c8aceb1 100644 --- a/src/Color.ts +++ b/src/Color.ts @@ -1,8 +1,16 @@ -const clamp = (n: number, min: number, max: number) => +const clamp = (n: number, min: number, max: number): number => Math.max(min, Math.min(max, n)); // https://www.rapidtables.com/convert/color/rgb-to-hsv.html -function rgbToHSV(r: number, g: number, b: number) { +function rgbToHSV( + r: number, + g: number, + b: number +): { + h: number; + s: number; + v: number; +} { r /= 255; g /= 255; b /= 255; @@ -37,7 +45,11 @@ function rgbToHSV(r: number, g: number, b: number) { } // https://www.rapidtables.com/convert/color/hsv-to-rgb.html -function hsvToRGB(h: number, s: number, v: number) { +function hsvToRGB( + h: number, + s: number, + v: number +): { r: number; g: number; b: number } { h = (h / 100) * 360; s /= 100; v /= 100; @@ -91,16 +103,16 @@ export default class Color { this.a = a; } - static rgb(r: number, g: number, b: number, a = 1) { + static rgb(r: number, g: number, b: number, a = 1): Color { const { h, s, v } = rgbToHSV(r, g, b); return new Color(h, s, v, a); } - static hsv(h: number, s: number, v: number, a = 1) { + static hsv(h: number, s: number, v: number, a = 1): Color { return new Color(h, s, v, a); } - static num(n: number | string) { + static num(n: number | string): Color { n = Number(n); // Match Scratch rgba system @@ -113,7 +125,7 @@ export default class Color { } // Red - get r() { + get r(): number { return hsvToRGB(this.h, this.s, this.v).r; } set r(r) { @@ -121,7 +133,7 @@ export default class Color { } // Green - get g() { + get g(): number { return hsvToRGB(this.h, this.s, this.v).g; } set g(g) { @@ -129,7 +141,7 @@ export default class Color { } // Blue - get b() { + get b(): number { return hsvToRGB(this.h, this.s, this.v).b; } set b(b) { @@ -137,7 +149,7 @@ export default class Color { } // Alpha - get a() { + get a(): number { return this._a; } set a(a) { @@ -145,7 +157,7 @@ export default class Color { } // Hue - get h() { + get h(): number { return this._h; } set h(h) { @@ -153,7 +165,7 @@ export default class Color { } // Shade - get s() { + get s(): number { return this._s; } set s(s) { @@ -161,14 +173,14 @@ export default class Color { } // Value - get v() { + get v(): number { return this._v; } set v(v) { this._v = clamp(v, 0, 100); } - _setRGB(r: number, g: number, b: number) { + _setRGB(r: number, g: number, b: number): void { r = clamp(r, 0, 255); g = clamp(g, 0, 255); b = clamp(b, 0, 255); @@ -180,8 +192,8 @@ export default class Color { this.v = v; } - toHexString(forceIncludeAlpha = false) { - const toHexDigits = (n: number) => { + toHexString(forceIncludeAlpha = false): string { + const toHexDigits = (n: number): string => { n = clamp(Math.round(n), 0, 255); let str = n.toString(16); @@ -200,7 +212,7 @@ export default class Color { return hex; } - toRGBString(forceIncludeAlpha = false) { + toRGBString(forceIncludeAlpha = false): string { const rgb = [this.r, this.g, this.b].map(Math.round); if (forceIncludeAlpha || this.a !== 1) { @@ -219,7 +231,7 @@ export default class Color { return [rgb.r / 255, rgb.g / 255, rgb.b / 255, this._a]; } - toString() { + toString(): string { return this.toRGBString(); } } diff --git a/src/Costume.ts b/src/Costume.ts index d5f087d..e312c44 100644 --- a/src/Costume.ts +++ b/src/Costume.ts @@ -20,11 +20,11 @@ export default class Costume { this.center = center; } - get width() { + get width(): number { return this.img.naturalWidth; } - get height() { + get height(): number { return this.img.naturalHeight; } } diff --git a/src/Input.ts b/src/Input.ts index 9c455a6..840545a 100644 --- a/src/Input.ts +++ b/src/Input.ts @@ -1,11 +1,13 @@ import type { Stage } from "./Sprite.js"; +type Mouse = { x: number; y: number; down: boolean }; + export default class Input { _stage: Stage; _canvas: HTMLCanvasElement; _onKeyDown: (key: string) => unknown; - mouse: { x: number; y: number; down: boolean }; + mouse: Mouse; keys: string[]; constructor( stage: Input["_stage"], @@ -32,7 +34,7 @@ export default class Input { this._onKeyDown = onKeyDown; } - _mouseMove(e: MouseEvent) { + _mouseMove(e: MouseEvent): void { const rect = this._canvas.getBoundingClientRect(); const scaleX = this._stage.width / rect.width; const scaleY = this._stage.height / rect.height; @@ -48,26 +50,26 @@ export default class Input { }; } - _mouseDown() { + _mouseDown(): void { this.mouse = { ...this.mouse, down: true, }; } - _mouseUp() { + _mouseUp(): void { this.mouse = { ...this.mouse, down: false, }; } - _keyup(e: KeyboardEvent) { + _keyup(e: KeyboardEvent): void { const key = this._getKeyName(e); this.keys = this.keys.filter((k) => k !== key); } - _keydown(e: KeyboardEvent) { + _keydown(e: KeyboardEvent): void { e.preventDefault(); const key = this._getKeyName(e); @@ -78,7 +80,7 @@ export default class Input { this._onKeyDown(key); } - _getKeyName(e: KeyboardEvent) { + _getKeyName(e: KeyboardEvent): string { if (e.key === "ArrowUp") return "up arrow"; if (e.key === "ArrowDown") return "down arrow"; if (e.key === "ArrowLeft") return "left arrow"; @@ -89,12 +91,14 @@ export default class Input { return e.key.toLowerCase(); } - keyPressed(name: string) { + keyPressed(name: string): boolean { if (name === "any") return this.keys.length > 0; return this.keys.indexOf(name) > -1; } - focus() { + focus(): void { this._canvas.focus(); } } + +export type { Mouse }; diff --git a/src/Loudness.ts b/src/Loudness.ts index 2e215c9..f780e80 100644 --- a/src/Loudness.ts +++ b/src/Loudness.ts @@ -26,11 +26,11 @@ export default class LoudnessHandler { this.connectionState = ConnectionState.NOT_CONNECTED; } - get audioContext() { + get audioContext(): AudioContext { return Sound.audioContext; } - async connect() { + async connect(): Promise { // If we're in the middle of connecting, or failed to connect, // don't attempt to connect again if (this.connectionState !== ConnectionState.NOT_CONNECTED) return; @@ -57,7 +57,7 @@ export default class LoudnessHandler { } } - get loudness() { + get loudness(): number { if ( this.connectionState !== ConnectionState.CONNECTED || !this.audioStream?.active || @@ -88,7 +88,7 @@ export default class LoudnessHandler { return rms; } - getLoudness() { + getLoudness(): number { void this.connect(); return this.loudness; } diff --git a/src/Project.ts b/src/Project.ts index 1b6d2f2..2685383 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -5,7 +5,7 @@ import LoudnessHandler from "./Loudness.js"; import Sound from "./Sound.js"; import type { Stage, Sprite } from "./Sprite.js"; -type RunningTrigger = { +type TriggerWithTarget = { target: Sprite | Stage; trigger: Trigger; }; @@ -19,8 +19,8 @@ export default class Project { loudnessHandler: LoudnessHandler; _cachedLoudness: number | null; - runningTriggers: RunningTrigger[]; - _prevStepTriggerPredicates: WeakMap; + runningTriggers: TriggerWithTarget[]; + _prevStepTriggerPredicates: WeakMap; answer: string | null; timerStart!: Date; @@ -62,7 +62,7 @@ export default class Project { this._renderLoop(); } - attach(renderTarget: string | HTMLElement) { + attach(renderTarget: string | HTMLElement): void { this.renderer.setRenderTarget(renderTarget); this.renderer.stage.addEventListener("click", () => { // Chrome requires a user gesture on the page before we can start the @@ -92,7 +92,7 @@ export default class Project { }); } - greenFlag() { + greenFlag(): void { // Chrome requires a user gesture on the page before we can start the // audio context. // When greenFlag is triggered, it's likely that the cause of it was some @@ -107,7 +107,7 @@ export default class Project { // Find triggers which match the given condition _matchingTriggers( triggerMatches: (tr: Trigger, target: Sprite | Stage) => boolean - ) { + ): TriggerWithTarget[] { const matchingTriggers = []; const targets = this.spritesAndStage; for (const target of targets) { @@ -121,7 +121,7 @@ export default class Project { return matchingTriggers; } - _stepEdgeActivatedTriggers() { + _stepEdgeActivatedTriggers(): void { const edgeActivated = this._matchingTriggers((tr) => tr.isEdgeActivated); const triggersToStart = []; for (const triggerWithTarget of edgeActivated) { @@ -152,7 +152,7 @@ export default class Project { void this._startTriggers(triggersToStart); } - step() { + step(): void { this._cachedLoudness = null; this._stepEdgeActivatedTriggers(); @@ -168,7 +168,7 @@ export default class Project { ); } - render() { + render(): void { // Render to canvas this.renderer.update(); @@ -182,12 +182,12 @@ export default class Project { } } - _renderLoop() { + _renderLoop(): void { requestAnimationFrame(this._renderLoop.bind(this)); this.render(); } - fireTrigger(trigger: symbol, options?: TriggerOptions) { + fireTrigger(trigger: symbol, options?: TriggerOptions): Promise { // Special trigger behaviors if (trigger === Trigger.GREEN_FLAG) { this.restartTimer(); @@ -212,7 +212,7 @@ export default class Project { return this._startTriggers(matchingTriggers); } - _startTriggers(triggers: RunningTrigger[]) { + _startTriggers(triggers: TriggerWithTarget[]): Promise { // Only add these triggers to this.runningTriggers if they're not already there. // TODO: if the triggers are already running, they'll be restarted but their execution order is unchanged. // Does that match Scratch's behavior? @@ -231,16 +231,16 @@ export default class Project { triggers.map(({ trigger, target }) => { return trigger.start(target); }) - ); + ).then(); } - get spritesAndClones() { + get spritesAndClones(): Sprite[] { return Object.values(this.sprites) .flatMap((sprite) => sprite!.andClones()) .sort((a, b) => a._layerOrder - b._layerOrder); } - get spritesAndStage() { + get spritesAndStage(): [...Sprite[], Stage] { return [...this.spritesAndClones, this.stage]; } @@ -248,7 +248,7 @@ export default class Project { sprite: Sprite, layerDelta: number, relativeToSprite = sprite - ) { + ): void { const spritesArray = this.spritesAndClones; const originalIndex = spritesArray.indexOf(sprite); @@ -270,26 +270,26 @@ export default class Project { }); } - stopAllSounds() { + stopAllSounds(): void { for (const target of this.spritesAndStage) { target.stopAllOfMySounds(); } } - get timer() { + get timer(): number { const ms = new Date().getTime() - this.timerStart.getTime(); return ms / 1000; } - restartTimer() { + restartTimer(): void { this.timerStart = new Date(); } - async askAndWait(question: string) { + async askAndWait(question: string): Promise { this.answer = await this.renderer.displayAskBox(question); } - get loudness() { + get loudness(): number { if (this._cachedLoudness === null) { this._cachedLoudness = this.loudnessHandler.getLoudness(); } diff --git a/src/Renderer.ts b/src/Renderer.ts index a0ce7b2..98ed7b2 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -222,7 +222,7 @@ export default class Renderer { return framebufferInfo; } - _setShader(shader: Shader) { + _setShader(shader: Shader): boolean { if (shader !== this._currentShader) { const gl = this.gl; gl.useProgram(shader.program); @@ -251,7 +251,7 @@ export default class Renderer { return false; } - _setFramebuffer(framebufferInfo: FramebufferInfo | null) { + _setFramebuffer(framebufferInfo: FramebufferInfo | null): void { if (framebufferInfo !== this._currentFramebuffer) { this._currentFramebuffer = framebufferInfo; if (framebufferInfo === null) { @@ -269,7 +269,7 @@ export default class Renderer { } } - setRenderTarget(renderTarget: HTMLElement | string | null) { + setRenderTarget(renderTarget: HTMLElement | string | null): void { if (typeof renderTarget === "string") { renderTarget = document.querySelector(renderTarget) as HTMLElement; } @@ -288,7 +288,7 @@ export default class Renderer { optionsIn: { filter?: (layer: Sprite | Stage | PenSkin) => boolean; } & Partial = {} - ) { + ): void { const options = { drawMode: ShaderManager.DrawModes.DEFAULT, renderSpeechBubbles: true, @@ -299,7 +299,7 @@ export default class Renderer { // If we're given a filter function in the options, filter by that too. // If we're given both, then only include layers which match both. const shouldRestrictLayers = layers instanceof Set; - const shouldIncludeLayer = (layer: Sprite | Stage | PenSkin) => + const shouldIncludeLayer = (layer: Sprite | Stage | PenSkin): boolean => !( (shouldRestrictLayers && !layers.has(layer)) || (typeof options.filter === "function" && !options.filter(layer)) @@ -338,7 +338,7 @@ export default class Renderer { } } - _updateStageSize() { + _updateStageSize(): void { if (this._currentShader) { // The shader is passed things in "Scratch-space" (-240, 240) and (-180, 180). // This tells it those dimensions so it can convert them to OpenGL "clip-space" (-1, 1). @@ -360,7 +360,7 @@ export default class Renderer { } // Keep the canvas size in sync with the CSS size. - _resize() { + _resize(): void { const stageSize = this.stage.getBoundingClientRect(); const ratio = window.devicePixelRatio; const adjustedWidth = Math.round(stageSize.width * ratio); @@ -380,7 +380,7 @@ export default class Renderer { } } - update() { + update(): void { this._resize(); // Draw to the screen, not to a framebuffer. @@ -394,7 +394,7 @@ export default class Renderer { this._renderLayers(); } - createStage(w: number, h: number) { + createStage(w: number, h: number): HTMLCanvasElement { const stage = document.createElement("canvas"); stage.width = w; stage.height = h; @@ -417,7 +417,7 @@ export default class Renderer { _calculateSpeechBubbleMatrix( spr: Sprite, speechBubbleSkin: SpeechBubbleSkin - ) { + ): MatrixType { const sprBounds = this.getBoundingBox(spr); let x; if ( @@ -449,7 +449,7 @@ export default class Renderer { effectMask?: number, colorMask?: [number, number, number, number], spriteColorId?: number - ) { + ): void { const gl = this.gl; const skinTexture = skin.getTexture(scale * this._screenSpaceScale); @@ -500,7 +500,7 @@ export default class Renderer { this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); } - renderSprite(sprite: Sprite | Stage, options: RenderSpriteOptions) { + renderSprite(sprite: Sprite | Stage, options: RenderSpriteOptions): void { const spriteScale = "size" in sprite ? sprite.size / 100 : 1; this._renderSkin( @@ -533,16 +533,16 @@ export default class Renderer { } } - getTightBoundingBox(sprite: Sprite | Stage) { + getTightBoundingBox(sprite: Sprite | Stage): Rectangle { return this._getDrawable(sprite).getTightBoundingBox(); } - getBoundingBox(sprite: Sprite | Stage) { + getBoundingBox(sprite: Sprite | Stage): Rectangle { return Rectangle.fromMatrix(this._getDrawable(sprite).getMatrix()); } // Mask drawing in to only areas where this sprite is opaque. - _stencilSprite(spr: Sprite | Stage, colorMask?: Color) { + _stencilSprite(spr: Sprite | Stage, colorMask?: Color): void { const gl = this.gl; gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); @@ -597,7 +597,7 @@ export default class Renderer { targets: Set | (Sprite | Stage)[] | Sprite | Stage, fast?: boolean, sprColor?: Color - ) { + ): boolean { if ("visible" in spr && !spr.visible) return false; if (!(targets instanceof Set)) { if (targets instanceof Array) { @@ -680,7 +680,7 @@ export default class Renderer { spr: Sprite | Stage, targetsColor: Color, sprColor?: Color - ) { + ): boolean { const sprBox = Rectangle.copy( this.getBoundingBox(spr), __collisionBox @@ -738,7 +738,10 @@ export default class Renderer { } // Pick the topmost sprite at the given point (if one exists). - pick(sprites: (Sprite | Stage)[], point: { x: number; y: number }) { + pick( + sprites: (Sprite | Stage)[], + point: { x: number; y: number } + ): Sprite | Stage | null { this._setFramebuffer(this._collisionBuffer); const gl = this.gl; gl.clearColor(0, 0, 0, 0); @@ -778,7 +781,7 @@ export default class Renderer { spr: Sprite, point: { x: number; y: number }, fast?: boolean - ) { + ): boolean { if (!spr.visible) return false; const box = this.getBoundingBox(spr); @@ -813,15 +816,15 @@ export default class Renderer { pt2: { x: number; y: number }, color: Color, size: number - ) { + ): void { this._penSkin.penLine(pt1, pt2, color, size); } - clearPen() { + clearPen(): void { this._penSkin.clear(); } - stamp(spr: Sprite | Stage) { + stamp(spr: Sprite | Stage): void { this._setFramebuffer(this._penSkin._framebufferInfo); this._renderLayers(new Set([spr]), { renderSpeechBubbles: false }); } diff --git a/src/Sound.ts b/src/Sound.ts index b3fda30..b843bd1 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -1,4 +1,5 @@ import decodeADPCMAudio, { isADPCMData } from "./lib/decode-adpcm-audio.js"; +import type { Yielding } from "./lib/yielding.js"; export default class Sound { name: string; @@ -25,11 +26,11 @@ export default class Sound { void this.downloadMyAudioBuffer(); } - get duration() { + get duration(): number { return this.audioBuffer ? this.audioBuffer.duration : 0; } - *start() { + *start(): Yielding { let started = false; let isLatestCallToStart = true; @@ -63,7 +64,7 @@ export default class Sound { // finish playing. Of course, the latest call returns true, and so the // containing playUntilDone() (if present) knows to wait. const oldDoneDownloading = this._doneDownloading; - this._doneDownloading = (fromMoreRecentCall) => { + this._doneDownloading = (fromMoreRecentCall): void => { if (fromMoreRecentCall) { isLatestCallToStart = false; } else { @@ -82,7 +83,7 @@ export default class Sound { return isLatestCallToStart; } - *playUntilDone() { + *playUntilDone(): Yielding { let playing = true; const isLatestCallToStart = yield* this.start(); @@ -109,7 +110,7 @@ export default class Sound { // is meant to be interrupted if another start() is ran while it's playing. // Of course, we don't want *this* playUntilDone() to be treated as though it // were interrupted when we call start(), so setting _markDone comes after. - this._markDone = () => { + this._markDone = (): void => { playing = false; delete this._markDone; }; @@ -117,7 +118,7 @@ export default class Sound { while (playing) yield; } - stop() { + stop(): void { if (this._markDone) { this._markDone(); } @@ -128,7 +129,7 @@ export default class Sound { } } - downloadMyAudioBuffer() { + downloadMyAudioBuffer(): Promise { return fetch(this.url) .then((body) => body.arrayBuffer()) .then((arrayBuffer) => { @@ -161,7 +162,7 @@ export default class Sound { }); } - playMyAudioBuffer() { + playMyAudioBuffer(): void { if (!this.audioBuffer) { return; } @@ -181,7 +182,7 @@ export default class Sound { this.source.start(Sound.audioContext.currentTime); } - connect(target: AudioNode) { + connect(target: AudioNode): void { if (target !== this.target) { this.target = target; if (this.source) { @@ -191,14 +192,14 @@ export default class Sound { } } - setPlaybackRate(value: number) { + setPlaybackRate(value: number): void { this.playbackRate = value; if (this.source) { this.source.playbackRate.value = value; } } - isConnectedTo(target: AudioNode) { + isConnectedTo(target: AudioNode): boolean { return this.target === target; } @@ -215,7 +216,7 @@ export default class Sound { return this._audioContext; } - static decodeADPCMAudio(audioBuffer: ArrayBuffer) { + static decodeADPCMAudio(audioBuffer: ArrayBuffer): Promise { return decodeADPCMAudio(audioBuffer, this.audioContext); } } @@ -367,7 +368,7 @@ export class EffectChain { this.getNonPatchSoundList = getNonPatchSoundList; } - resetToInitial() { + resetToInitial(): void { // Note: some effects won't be reset by this function, except for when they // are set for the first time (i.e. when the EffectChain is instantiated). // Look for the "reset: false" flag in the effect descriptor list. @@ -386,7 +387,7 @@ export class EffectChain { } } - updateAudioEffect(name: EffectName) { + updateAudioEffect(name: EffectName): void { const descriptor = EffectChain.getEffectDescriptor(name); if (!descriptor) { @@ -526,7 +527,7 @@ export class EffectChain { } } - connect(target: AudioNode) { + connect(target: AudioNode): void { this.target = target; // All the code here is basically the same as what's written in @@ -553,7 +554,7 @@ export class EffectChain { last.output.connect(target); } - setEffectValue(name: EffectName, value: number | string | boolean) { + setEffectValue(name: EffectName, value: number | string | boolean): void { value = Number(value); if ( name in this.effectValues && @@ -566,7 +567,7 @@ export class EffectChain { } } - changeEffectValue(name: EffectName, value: number | string | boolean) { + changeEffectValue(name: EffectName, value: number | string | boolean): void { value = Number(value); if (name in this.effectValues && !isNaN(value) && value !== 0) { this.effectValues[name] += value; @@ -575,7 +576,7 @@ export class EffectChain { } } - clampEffectValue(name: EffectName) { + clampEffectValue(name: EffectName): void { // Not all effects are clamped (pitch, for example); it's also possible to // specify only a minimum or maximum bound, instead of both. const descriptor = EffectChain.getEffectDescriptor(name); @@ -595,7 +596,7 @@ export class EffectChain { return this.effectValues[name] || 0; } - clone(newConfig: EffectChainConfig) { + clone(newConfig: EffectChainConfig): EffectChain { const newEffectChain = new EffectChain( Object.assign({}, this.config, newConfig) ); @@ -615,7 +616,7 @@ export class EffectChain { return newEffectChain; } - applyToSound(sound: Sound) { + applyToSound(sound: Sound): void { sound.connect(this.inputNode); for (const [name, value] of Object.entries(this.effectValues) as [ @@ -629,7 +630,7 @@ export class EffectChain { } } - isTargetOf(sound: Sound) { + isTargetOf(sound: Sound): boolean { return sound.isConnectedTo(this.inputNode); } @@ -654,15 +655,17 @@ export class EffectChain { )!; } - static getFirstEffectDescriptor() { + static getFirstEffectDescriptor(): typeof effectDescriptors[number] { return this.effectDescriptors[0]; } - static getLastEffectDescriptor() { + static getLastEffectDescriptor(): typeof effectDescriptors[number] { return this.effectDescriptors[this.effectDescriptors.length - 1]; } - static getNextEffectDescriptor(name: EffectName) { + static getNextEffectDescriptor( + name: EffectName + ): typeof effectDescriptors[number] | undefined { // .find() provides three values to its passed function: the value of the // current item, that item's index, and the array on which .find() is // operating. In this case, we're only concerned with the index. @@ -677,7 +680,9 @@ export class EffectChain { .find((_, i) => this.effectDescriptors[i].name === name); } - static getPreviousEffectDescriptor(name: EffectName) { + static getPreviousEffectDescriptor( + name: EffectName + ): typeof effectDescriptors[number] | undefined { // This function's a little simpler, since it doesn't involve shifting the // list. We still use slice(), but this time simply to cut off the last // item; that item will never come before any other, after all. We search @@ -749,7 +754,7 @@ export class AudioEffectMap { } } - clear() { + clear(): void { this.effectChain.resetToInitial(); } } diff --git a/src/Sprite.ts b/src/Sprite.ts index db482ea..3fb2f7d 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -2,8 +2,10 @@ import Color from "./Color.js"; import Trigger from "./Trigger.js"; import Sound, { EffectChain, AudioEffectMap } from "./Sound.js"; import Costume from "./Costume.js"; +import type { Mouse } from "./Input.js"; import type Project from "./Project.js"; import type Watcher from "./Watcher.js"; +import type { Yielding } from "./lib/yielding.js"; import { effectNames } from "./renderer/effectInfo.js"; @@ -61,7 +63,7 @@ export class _EffectMap implements Effects { } } - _clone() { + _clone(): _EffectMap { const m = new _EffectMap(); for (const effectName of Object.keys( this._effectValues @@ -71,7 +73,7 @@ export class _EffectMap implements Effects { return m; } - clear() { + clear(): void { for (const effectName of Object.keys( this._effectValues ) as (keyof typeof this._effectValues)[]) { @@ -133,23 +135,23 @@ abstract class SpriteBase { this._vars = vars ?? {}; } - getSoundsPlayedByMe() { + getSoundsPlayedByMe(): Sound[] { return this.sounds.filter((sound) => this.effectChain.isTargetOf(sound)); } - get stage() { + get stage(): Stage | undefined { return this._project?.stage; } - get sprites() { + get sprites(): Partial> | undefined { return this._project?.sprites; } - get vars() { + get vars(): Vars { return this._vars; } - get costumeNumber() { + get costumeNumber(): number { return this._costumeNumber; } @@ -221,27 +223,27 @@ abstract class SpriteBase { return this.costumes[this.costumeNumber - 1]; } - degToRad(deg: number) { + degToRad(deg: number): number { return (deg * Math.PI) / 180; } - radToDeg(rad: number) { + radToDeg(rad: number): number { return (rad * 180) / Math.PI; } - degToScratch(deg: number) { + degToScratch(deg: number): number { return -deg + 90; } - scratchToDeg(scratchDir: number) { + scratchToDeg(scratchDir: number): number { return -scratchDir + 90; } - radToScratch(rad: number) { + radToScratch(rad: number): number { return this.degToScratch(this.radToDeg(rad)); } - scratchToRad(scratchDir: number) { + scratchToRad(scratchDir: number): number { return this.degToRad(this.scratchToDeg(scratchDir)); } @@ -261,7 +263,7 @@ abstract class SpriteBase { } // Wrap rotation from -180 to 180. - normalizeDeg(deg: number) { + normalizeDeg(deg: number): number { // This is a pretty big math expression, but it's necessary because in JavaScript, // the % operator means "remainder", not "modulo", and so negative numbers won't "wrap around". // See https://web.archive.org/web/20090717035140if_/javascript.about.com/od/problemsolving/a/modulobug.htm @@ -288,7 +290,7 @@ abstract class SpriteBase { } // TODO: this should also take strings so rand("0.0", "1.0") returns a random float like Scratch - random(a: number, b: number) { + random(a: number, b: number): number { const min = Math.min(a, b); const max = Math.max(a, b); if (min % 1 === 0 && max % 1 === 0) { @@ -297,7 +299,7 @@ abstract class SpriteBase { return Math.random() * (max - min) + min; } - *wait(secs: number) { + *wait(secs: number): Yielding { const endTime = new Date(); endTime.setMilliseconds(endTime.getMilliseconds() + secs * 1000); while (new Date() < endTime) { @@ -305,23 +307,23 @@ abstract class SpriteBase { } } - get mouse() { + get mouse(): Mouse | undefined { return this._project?.input.mouse; } - keyPressed(name: string) { - return this._project?.input.keyPressed(name); + keyPressed(name: string): boolean { + return !!this._project?.input.keyPressed(name); } - get timer() { + get timer(): number | undefined { return this._project?.timer; } - restartTimer() { + restartTimer(): void { this._project?.restartTimer(); } - *startSound(soundName: string) { + *startSound(soundName: string): Yielding { const sound = this.getSound(soundName); if (sound) { this.effectChain.applyToSound(sound); @@ -329,7 +331,7 @@ abstract class SpriteBase { } } - *playSoundUntilDone(soundName: string) { + *playSoundUntilDone(soundName: string): Yielding { const sound = this.getSound(soundName); if (sound) { sound.connect(this.effectChain.inputNode); @@ -338,7 +340,7 @@ abstract class SpriteBase { } } - getSound(soundName: string) { + getSound(soundName: string): Sound | undefined { if (typeof soundName === "number") { return this.sounds[(soundName - 1) % this.sounds.length]; } else { @@ -346,21 +348,24 @@ abstract class SpriteBase { } } - stopAllSounds() { + stopAllSounds(): void { this._project?.stopAllSounds(); } - stopAllOfMySounds() { + stopAllOfMySounds(): void { for (const sound of this.sounds) { sound.stop(); } } - broadcast(name: string) { - return this._project?.fireTrigger(Trigger.BROADCAST, { name }); + broadcast(name: string): Promise { + // TODO: definitely assign _project then remove this + return this._project + ? this._project.fireTrigger(Trigger.BROADCAST, { name }) + : Promise.resolve(); } - *broadcastAndWait(name: string) { + *broadcastAndWait(name: string): Yielding { let running = true; void this.broadcast(name)?.then(() => { running = false; @@ -371,11 +376,11 @@ abstract class SpriteBase { } } - clearPen() { + clearPen(): void { this._project?.renderer.clearPen(); } - *askAndWait(question: string) { + *askAndWait(question: string): Yielding { if (this._speechBubble && this instanceof Sprite) { this.say(""); } @@ -388,12 +393,12 @@ abstract class SpriteBase { while (!done) yield; } - get answer() { - return this._project?.answer; + get answer(): string | null { + return this._project?.answer ?? null; } - get loudness() { - return this._project?.loudness; + get loudness(): number { + return this._project?.loudness ?? -1; } toNumber(value) { @@ -562,7 +567,7 @@ export class Sprite extends SpriteBase { }; } - createClone() { + createClone(): void { const clone = Object.assign( Object.create(Object.getPrototypeOf(this) as object) as Sprite, this @@ -612,7 +617,7 @@ export class Sprite extends SpriteBase { ); } - deleteThisClone() { + deleteThisClone(): void { if (this.parent === null) return; this.parent.clones = this.parent.clones.filter((clone) => clone !== this); @@ -629,7 +634,7 @@ export class Sprite extends SpriteBase { return [this, ...this.clones.flatMap((clone) => clone.andClones())]; } - get direction() { + get direction(): number { return this._direction; } @@ -637,7 +642,7 @@ export class Sprite extends SpriteBase { this._direction = this.normalizeDeg(dir); } - goto(x: number, y: number) { + goto(x: number, y: number): void { if (x === this.x && y === this.y) return; if (this.penDown && this._project) { @@ -653,7 +658,7 @@ export class Sprite extends SpriteBase { this._y = y; } - get x() { + get x(): number { return this._x; } @@ -661,7 +666,7 @@ export class Sprite extends SpriteBase { this.goto(x, this._y); } - get y() { + get y(): number { return this._y; } @@ -669,7 +674,7 @@ export class Sprite extends SpriteBase { this.goto(this._x, y); } - move(dist: number) { + move(dist: number): void { const moveDir = this.scratchToRad(this.direction); this.goto( @@ -678,8 +683,9 @@ export class Sprite extends SpriteBase { ); } - *glide(seconds: number, x: number, y: number) { - const interpolate = (a: number, b: number, t: number) => a + (b - a) * t; + *glide(seconds: number, x: number, y: number): Yielding { + const interpolate = (a: number, b: number, t: number): number => + a + (b - a) * t; const startTime = new Date(); const startX = this._x; @@ -693,7 +699,7 @@ export class Sprite extends SpriteBase { } while (t < 1); } - moveAhead(value = Infinity) { + moveAhead(value = Infinity): void { if (typeof value === "number") { this._project?.changeSpriteLayer(this, value); } else { @@ -701,7 +707,7 @@ export class Sprite extends SpriteBase { } } - moveBehind(value = Infinity) { + moveBehind(value = Infinity): void { if (typeof value === "number") { this._project?.changeSpriteLayer(this, -value); } else { @@ -709,7 +715,7 @@ export class Sprite extends SpriteBase { } } - get penDown() { + get penDown(): boolean { return this._penDown; } @@ -725,7 +731,7 @@ export class Sprite extends SpriteBase { this._penDown = penDown; } - get penColor() { + get penColor(): Color { return this._penColor; } @@ -739,7 +745,7 @@ export class Sprite extends SpriteBase { } } - stamp() { + stamp(): void { this._project?.renderer.stamp(this); } @@ -813,17 +819,17 @@ export class Sprite extends SpriteBase { } } - say(text: string) { + say(text: string): void { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); this._speechBubble = { text: String(text), style: "say", timeout: null }; } - think(text: string) { + think(text: string): void { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); this._speechBubble = { text: String(text), style: "think", timeout: null }; } - *sayAndWait(text: string, seconds: number) { + *sayAndWait(text: string, seconds: number): Yielding { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); const speechBubble: SpeechBubble = { text, style: "say", timeout: null }; @@ -839,7 +845,7 @@ export class Sprite extends SpriteBase { while (!done) yield; } - *thinkAndWait(text: string, seconds: number) { + *thinkAndWait(text: string, seconds: number): Yielding { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); const speechBubble: SpeechBubble = { text, style: "think", timeout: null }; @@ -892,9 +898,12 @@ export class Stage extends SpriteBase { this.__counter = 0; } - fireBackdropChanged() { - return this._project?.fireTrigger(Trigger.BACKDROP_CHANGED, { - backdrop: this.costume.name, - }); + fireBackdropChanged(): Promise { + // TODO: definitely assign _project then remove this + return this._project + ? this._project.fireTrigger(Trigger.BACKDROP_CHANGED, { + backdrop: this.costume.name, + }) + : Promise.resolve(); } } diff --git a/src/Trigger.ts b/src/Trigger.ts index 3cb416b..fc8a581 100644 --- a/src/Trigger.ts +++ b/src/Trigger.ts @@ -45,7 +45,7 @@ export default class Trigger { this.stop = () => {}; } - get isEdgeActivated() { + get isEdgeActivated(): boolean { return ( this.trigger === TIMER_GREATER_THAN || this.trigger === LOUDNESS_GREATER_THAN @@ -54,7 +54,10 @@ export default class Trigger { // Evaluate the given trigger option, whether it's a value or a function that // returns a value given a target - option(option: string, target: Sprite | Stage) { + option( + option: string, + target: Sprite | Stage + ): number | string | boolean | undefined { const triggerOption = this.options[option]; // If the given option is a function, evaluate that function, passing in // the target that we're evaluating the trigger for @@ -68,7 +71,7 @@ export default class Trigger { trigger: Trigger["trigger"], options: Trigger["options"] | undefined, target: Sprite | Stage - ) { + ): boolean { if (this.trigger !== trigger) return false; for (const option in options) { if (this.option(option, target) !== options[option]) return false; @@ -77,7 +80,7 @@ export default class Trigger { return true; } - start(target: Sprite | Stage) { + start(target: Sprite | Stage): Promise { this.stop(); const boundScript = this._script.bind(target); @@ -86,43 +89,27 @@ export default class Trigger { this._runningScript = boundScript(); return new Promise((resolve) => { - this.stop = () => { + this.stop = (): void => { this.done = true; resolve(); }; }); } - step() { + step(): void { if (!this._runningScript) return; this.done = !!this._runningScript.next().done; if (this.done) this.stop(); } - static get GREEN_FLAG() { - return GREEN_FLAG; - } - static get KEY_PRESSED() { - return KEY_PRESSED; - } - static get BROADCAST() { - return BROADCAST; - } - static get CLICKED() { - return CLICKED; - } - static get CLONE_START() { - return CLONE_START; - } - static get LOUDNESS_GREATER_THAN() { - return LOUDNESS_GREATER_THAN; - } - static get TIMER_GREATER_THAN() { - return TIMER_GREATER_THAN; - } - static get BACKDROP_CHANGED() { - return BACKDROP_CHANGED; - } + static GREEN_FLAG = GREEN_FLAG; + static KEY_PRESSED = KEY_PRESSED; + static BROADCAST = BROADCAST; + static CLICKED = CLICKED; + static CLONE_START = CLONE_START; + static LOUDNESS_GREATER_THAN = LOUDNESS_GREATER_THAN; + static TIMER_GREATER_THAN = TIMER_GREATER_THAN; + static BACKDROP_CHANGED = BACKDROP_CHANGED; } export type { TriggerOption, TriggerOptions }; diff --git a/src/Watcher.ts b/src/Watcher.ts index e11c7b4..4a3e158 100644 --- a/src/Watcher.ts +++ b/src/Watcher.ts @@ -8,11 +8,13 @@ type WatcherValue = | undefined | (string | number | boolean | null | undefined)[]; +type WatcherStyle = "normal" | "large" | "slider"; + type WatcherOptions = { value?: () => WatcherValue; setValue?: (value: number) => void; label: string; - style?: "normal" | "large" | "slider"; + style?: WatcherStyle; visible?: boolean; color?: Color; step?: number; @@ -35,7 +37,7 @@ export default class Watcher { _min!: number; _max!: number; _step!: number; - _style!: "normal" | "large" | "slider"; + _style!: WatcherStyle; _visible!: boolean; _dom!: { @@ -84,7 +86,7 @@ export default class Watcher { this.step = 1; } - initializeDOM() { + initializeDOM(): void { const node = document.createElement("div"); node.classList.add("leopard__watcher"); @@ -109,7 +111,7 @@ export default class Watcher { this._dom = { node, label, value, slider }; } - updateDOM(renderTarget: HTMLElement) { + updateDOM(renderTarget: HTMLElement): void { if (renderTarget && !renderTarget.contains(this._dom.node)) { renderTarget.append(this._dom.node); } @@ -175,7 +177,7 @@ export default class Watcher { this._dom.value.style.setProperty("--watcher-text-color", textColor); } - get visible() { + get visible(): boolean { return this._visible; } set visible(visible) { @@ -183,7 +185,7 @@ export default class Watcher { this._dom.node.style.visibility = visible ? "visible" : "hidden"; } - get x() { + get x(): number { return this._x; } set x(x) { @@ -191,7 +193,7 @@ export default class Watcher { this._dom.node.style.left = `${x - 240}px`; } - get y() { + get y(): number { return this._y; } set y(y) { @@ -199,7 +201,7 @@ export default class Watcher { this._dom.node.style.top = `${180 - y}px`; } - get width() { + get width(): number | undefined { return this._width; } set width(width) { @@ -211,7 +213,7 @@ export default class Watcher { } } - get height() { + get height(): number | undefined { return this._height; } set height(height) { @@ -223,7 +225,7 @@ export default class Watcher { } } - get style() { + get style(): WatcherStyle { return this._style; } set style(style) { @@ -258,7 +260,7 @@ export default class Watcher { this._dom.slider.max = String(max); } - get step() { + get step(): number { return this._step; } set step(step) { @@ -266,7 +268,7 @@ export default class Watcher { this._dom.slider.step = String(step); } - get label() { + get label(): string { return this._label; } set label(label) { diff --git a/src/lib/decode-adpcm-audio.ts b/src/lib/decode-adpcm-audio.ts index 1c43a37..a7fc8f0 100644 --- a/src/lib/decode-adpcm-audio.ts +++ b/src/lib/decode-adpcm-audio.ts @@ -35,7 +35,7 @@ const ADPCM_INDEX = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8]; export default function decodeADPCMAudio( ab: ArrayBuffer, audioContext: AudioContext -) { +): Promise { const dv = new DataView(ab); // WAV magic number if (dv.getUint32(0) !== 0x52494646 || dv.getUint32(8) !== 0x57415645) { @@ -124,14 +124,14 @@ export default function decodeADPCMAudio( return Promise.reject(new Error(`Unrecognized WAV format ${format}`)); } -export function isWavData(arrayBuffer: ArrayBuffer) { +export function isWavData(arrayBuffer: ArrayBuffer): boolean { const dataView = new DataView(arrayBuffer); return ( dataView.getUint32(0) === 0x52494646 && dataView.getUint32(8) === 0x57415645 ); } -export function isADPCMData(arrayBuffer: ArrayBuffer) { +export function isADPCMData(arrayBuffer: ArrayBuffer): boolean { const dataView = new DataView(arrayBuffer); const format = dataView.getUint16(20, true); return isWavData(arrayBuffer) && format === 17; diff --git a/src/lib/yielding.ts b/src/lib/yielding.ts new file mode 100644 index 0000000..317eb8b --- /dev/null +++ b/src/lib/yielding.ts @@ -0,0 +1,7 @@ +/** + * Utility type for a generator function that yields nothing until eventually + * resolving to a value. Used extensively in Leopard and defined here so we + * don't have to type out the full definition each time (and also so I don't + * have to go back and change it everywhere if this type turns out to be wrong). + */ +export type Yielding = Generator; diff --git a/src/renderer/BitmapSkin.ts b/src/renderer/BitmapSkin.ts index 7a14114..1e26acf 100644 --- a/src/renderer/BitmapSkin.ts +++ b/src/renderer/BitmapSkin.ts @@ -16,7 +16,7 @@ export default class BitmapSkin extends Skin { this._setSizeFromImage(image); } - getImageData() { + getImageData(): ImageData | null { // Make sure to handle potentially non-loaded textures if (!this._image.complete) return null; @@ -34,7 +34,7 @@ export default class BitmapSkin extends Skin { return this._imageData; } - getTexture() { + getTexture(): WebGLTexture | null { // Make sure to handle potentially non-loaded textures const image = this._image; if (!image.complete) return null; @@ -46,7 +46,7 @@ export default class BitmapSkin extends Skin { return this._texture; } - destroy() { + destroy(): void { if (this._texture !== null) this.gl.deleteTexture(this._texture); } } diff --git a/src/renderer/Drawable.ts b/src/renderer/Drawable.ts index bb0b8ad..3af8366 100644 --- a/src/renderer/Drawable.ts +++ b/src/renderer/Drawable.ts @@ -3,6 +3,7 @@ import Matrix, { MatrixType } from "./Matrix.js"; import Rectangle from "./Rectangle.js"; import effectTransformPoint from "./effectTransformPoint.js"; import { effectBitmasks } from "./effectInfo.js"; +import type Skin from "./Skin.js"; import type Renderer from "../Renderer.js"; import { Sprite, Stage } from "../Sprite.js"; @@ -14,7 +15,7 @@ const determinant = ( a: [number, number], b: [number, number], c: [number, number] -) => { +): number => { return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); }; @@ -40,7 +41,7 @@ class SpriteTransformDiff { this.update(); } - update() { + update(): void { if (this._sprite instanceof Sprite) { this._lastX = this._sprite.x; this._lastY = this._sprite.y; @@ -53,7 +54,7 @@ class SpriteTransformDiff { this._unset = false; } - get changed() { + get changed(): boolean { return ( (this._sprite instanceof Sprite && (this._lastX !== this._sprite.x || @@ -114,18 +115,18 @@ export default class Drawable { this._convexHullMatrixDiff = new SpriteTransformDiff(sprite); } - getCurrentSkin() { + getCurrentSkin(): Skin { return this._renderer._getSkin(this._sprite.costume); } // Get the rough axis-aligned bounding box for this sprite. Not as tight as // getTightBoundingBox, especially when rotated. - getAABB() { + getAABB(): Rectangle { return Rectangle.fromMatrix(this.getMatrix(), this._aabb); } // Get the Scratch-space tight bounding box for this sprite. - getTightBoundingBox() { + getTightBoundingBox(): Rectangle { if (!this._convexHullMatrixDiff.changed) return this._tightBoundingBox; const matrix = this.getMatrix(); @@ -189,7 +190,7 @@ export default class Drawable { return this._tightBoundingBox; } - _calculateConvexHull() { + _calculateConvexHull(): [number, number][] | null { const sprite = this._sprite; const skin = this.getCurrentSkin(); const imageData = skin.getImageData( @@ -326,7 +327,7 @@ export default class Drawable { return this._convexHullPoints; } - _calculateSpriteMatrix() { + _calculateSpriteMatrix(): void { const m = this._matrix; Matrix.identity(m); const spr = this._sprite; @@ -368,7 +369,7 @@ export default class Drawable { this._matrixDiff.update(); } - getMatrix() { + getMatrix(): MatrixType { // If all the values we used to calculate the matrix haven't changed since // we last calculated the matrix, we can just return the matrix as-is. if (this._matrixDiff.changed) { diff --git a/src/renderer/Matrix.ts b/src/renderer/Matrix.ts index a699ccd..e4e8143 100644 --- a/src/renderer/Matrix.ts +++ b/src/renderer/Matrix.ts @@ -5,14 +5,14 @@ // 3x3 transform matrix operations, unrolled 4 da speedz. export default class Matrix { // Create a new 3x3 transform matrix, initialized to the identity matrix. - static create() { + static create(): MatrixType { const matrix = new Float32Array(9); Matrix.identity(matrix); return matrix; } // Reset a matrix to the identity matrix - static identity(dst: MatrixType) { + static identity(dst: MatrixType): MatrixType { dst[0] = 1; dst[1] = 0; dst[2] = 0; @@ -26,7 +26,12 @@ export default class Matrix { } // Translate a matrix by the given X and Y values - static translate(dst: MatrixType, src: MatrixType, x: number, y: number) { + static translate( + dst: MatrixType, + src: MatrixType, + x: number, + y: number + ): MatrixType { const a00 = src[0], a01 = src[1], a02 = src[2], @@ -52,7 +57,7 @@ export default class Matrix { } // Rotate a matrix, in radians - static rotate(dst: MatrixType, src: MatrixType, rad: number) { + static rotate(dst: MatrixType, src: MatrixType, rad: number): MatrixType { const a00 = src[0], a01 = src[1], a02 = src[2], @@ -80,7 +85,12 @@ export default class Matrix { } // Scale a matrix by the given X and Y values - static scale(dst: MatrixType, src: MatrixType, x: number, y: number) { + static scale( + dst: MatrixType, + src: MatrixType, + x: number, + y: number + ): MatrixType { dst[0] = x * src[0]; dst[1] = x * src[1]; dst[2] = x * src[2]; @@ -100,7 +110,7 @@ export default class Matrix { m: MatrixType, dst: [number, number], src: [number, number] - ) { + ): [number, number] { const x = src[0]; const y = src[1]; dst[0] = m[0] * x + m[3] * y + m[6]; diff --git a/src/renderer/PenSkin.ts b/src/renderer/PenSkin.ts index 4a8e2ce..37c0c4f 100644 --- a/src/renderer/PenSkin.ts +++ b/src/renderer/PenSkin.ts @@ -31,13 +31,13 @@ export default class PenSkin extends Skin { this.clear(); } - destroy() { + destroy(): void { const gl = this.gl; gl.deleteTexture(this._framebufferInfo.texture); gl.deleteFramebuffer(this._framebufferInfo.framebuffer); } - getTexture() { + getTexture(): WebGLTexture { return this._framebufferInfo.texture; } @@ -46,7 +46,7 @@ export default class PenSkin extends Skin { pt2: { x: number; y: number }, color: Color, size: number - ) { + ): void { const renderer = this.renderer; renderer._setFramebuffer(this._framebufferInfo); @@ -116,7 +116,7 @@ export default class PenSkin extends Skin { gl.drawArrays(gl.TRIANGLES, 0, 6); } - clear() { + clear(): void { this.renderer._setFramebuffer(this._framebufferInfo); const gl = this.gl; gl.clearColor(0, 0, 0, 0); diff --git a/src/renderer/Rectangle.ts b/src/renderer/Rectangle.ts index 7096d37..6e2ae2b 100644 --- a/src/renderer/Rectangle.ts +++ b/src/renderer/Rectangle.ts @@ -21,7 +21,7 @@ export default class Rectangle { bottom: number, top: number, result = new Rectangle() - ) { + ): Rectangle { result.left = left; result.right = right; result.bottom = bottom; @@ -31,7 +31,7 @@ export default class Rectangle { } // Initialize a bounding box around a sprite given the sprite's transform matrix. - static fromMatrix(matrix: MatrixType, result = new Rectangle()) { + static fromMatrix(matrix: MatrixType, result = new Rectangle()): Rectangle { // Adapted somewhat from https://github.com/LLK/scratch-render/blob/develop/docs/Rectangle-AABB-Matrix.md const xa = matrix[0] / 2; const xb = matrix[3] / 2; @@ -52,7 +52,7 @@ export default class Rectangle { } // Initialize from another rectangle. - static copy(src: Rectangle, dst: Rectangle) { + static copy(src: Rectangle, dst: Rectangle): Rectangle { dst.left = src.left; dst.right = src.right; dst.bottom = src.bottom; @@ -62,7 +62,7 @@ export default class Rectangle { // Push this rectangle out to integer bounds. // This takes a conservative approach and will always expand the rectangle outwards. - snapToInt() { + snapToInt(): this { this.left = Math.floor(this.left); this.right = Math.ceil(this.right); this.bottom = Math.floor(this.bottom); @@ -72,7 +72,7 @@ export default class Rectangle { } // Check whether any part of this rectangle touches another rectangle. - intersects(rect: Rectangle) { + intersects(rect: Rectangle): boolean { return ( this.left <= rect.right && rect.left <= this.right && @@ -82,14 +82,14 @@ export default class Rectangle { } // Check whether a given point is inside this rectangle. - containsPoint(x: number, y: number) { + containsPoint(x: number, y: number): boolean { return ( x >= this.left && x <= this.right && y >= this.bottom && y <= this.top ); } // Clamp this rectangle within bounds. - clamp(left: number, right: number, bottom: number, top: number) { + clamp(left: number, right: number, bottom: number, top: number): this { this.left = Math.min(Math.max(this.left, left), right); this.right = Math.max(Math.min(this.right, right), left); this.bottom = Math.min(Math.max(this.bottom, bottom), top); @@ -99,7 +99,11 @@ export default class Rectangle { } // Compute the union of two rectangles. - static union(rect1: Rectangle, rect2: Rectangle, result = new Rectangle()) { + static union( + rect1: Rectangle, + rect2: Rectangle, + result = new Rectangle() + ): Rectangle { result.left = Math.min(rect1.left, rect2.left); result.right = Math.max(rect1.right, rect2.right); result.bottom = Math.min(rect1.bottom, rect2.bottom); @@ -113,7 +117,7 @@ export default class Rectangle { rect1: Rectangle, rect2: Rectangle, result = new Rectangle() - ) { + ): Rectangle { result.left = Math.max(rect1.left, rect2.left); result.right = Math.min(rect1.right, rect2.right); result.bottom = Math.max(rect1.bottom, rect2.bottom); @@ -122,11 +126,11 @@ export default class Rectangle { return result; } - get width() { + get width(): number { return this.right - this.left; } - get height() { + get height(): number { return this.top - this.bottom; } } diff --git a/src/renderer/ShaderManager.ts b/src/renderer/ShaderManager.ts index 965c0aa..ccfcbe2 100644 --- a/src/renderer/ShaderManager.ts +++ b/src/renderer/ShaderManager.ts @@ -64,7 +64,7 @@ class ShaderManager { type: | WebGLRenderingContext["FRAGMENT_SHADER"] | WebGLRenderingContext["VERTEX_SHADER"] - ) { + ): WebGLShader { const gl = this.gl; const shader = gl.createShader(type); if (!shader) throw new Error("Could not create shader."); diff --git a/src/renderer/Skin.ts b/src/renderer/Skin.ts index 4d23179..6938c1d 100644 --- a/src/renderer/Skin.ts +++ b/src/renderer/Skin.ts @@ -55,7 +55,7 @@ export default abstract class Skin { } // Helper function to set this skin's size based on an image that may or may not be loaded. - _setSizeFromImage(image: HTMLImageElement) { + _setSizeFromImage(image: HTMLImageElement): void { if (image.complete) { this.width = image.naturalWidth; this.height = image.naturalHeight; diff --git a/src/renderer/SpeechBubbleSkin.ts b/src/renderer/SpeechBubbleSkin.ts index e3a6c56..3da6694 100644 --- a/src/renderer/SpeechBubbleSkin.ts +++ b/src/renderer/SpeechBubbleSkin.ts @@ -42,7 +42,7 @@ export default class SpeechBubbleSkin extends Skin { } // To ensure proper text measurement and drawing, it's necessary to restyle the canvas after resizing it. - _restyleCanvas() { + _restyleCanvas(): void { const ctx = this._ctx; ctx.font = "16px sans-serif"; ctx.textBaseline = "hanging"; @@ -53,7 +53,7 @@ export default class SpeechBubbleSkin extends Skin { this._rendered = false; } - _renderBubble(bubble: SpeechBubble, scale: number) { + _renderBubble(bubble: SpeechBubble, scale: number): void { const canvas = this._canvas; const ctx = this._ctx; @@ -64,7 +64,7 @@ export default class SpeechBubbleSkin extends Skin { h: number, r: number, style: "say" | "think" - ) => { + ): void => { if (r > w / 2) r = w / 2; if (r > h / 2) r = h / 2; if (r < 0) return; @@ -144,7 +144,7 @@ export default class SpeechBubbleSkin extends Skin { this._renderedScale = scale; } - getTexture(scale: number) { + getTexture(scale: number): WebGLTexture { if (!this._rendered || this._renderedScale !== scale) { this._renderBubble(this._bubble, scale); const gl = this.gl; @@ -162,7 +162,7 @@ export default class SpeechBubbleSkin extends Skin { return this._texture; } - destroy() { + destroy(): void { this.gl.deleteTexture(this._texture); } } diff --git a/src/renderer/VectorSkin.ts b/src/renderer/VectorSkin.ts index 7169748..4ce9c33 100644 --- a/src/renderer/VectorSkin.ts +++ b/src/renderer/VectorSkin.ts @@ -34,11 +34,11 @@ export default class VectorSkin extends Skin { this._mipmaps = new Map(); } - static mipLevelForScale(scale: number) { + static mipLevelForScale(scale: number): number { return Math.max(Math.ceil(Math.log2(scale)) + MIPMAP_OFFSET, 0); } - getImageData(scale: number) { + getImageData(scale: number): ImageData | null { if (!this._image.complete) return null; // Round off the scale of the image data drawn to a given power-of-two mip level. @@ -84,7 +84,7 @@ export default class VectorSkin extends Skin { // TODO: handle proper subpixel positioning when SVG viewbox has non-integer coordinates // This will require rethinking costume + project loading probably - _createMipmap(mipLevel: number) { + _createMipmap(mipLevel: number): void { // Instead of uploading the image to WebGL as a texture, render the image to a canvas and upload the canvas. const ctx = this._drawSvgToCanvas(mipLevel); this._mipmaps.set( @@ -95,7 +95,7 @@ export default class VectorSkin extends Skin { ); } - getTexture(scale: number) { + getTexture(scale: number): WebGLTexture | null { if (!this._image.complete) return null; // Because WebGL doesn't support vector graphics, substitute a bunch of bitmaps. @@ -111,7 +111,7 @@ export default class VectorSkin extends Skin { return this._mipmaps.get(mipLevel) ?? null; } - destroy() { + destroy(): void { for (const mip of this._mipmaps.values()) { this.gl.deleteTexture(mip); } From 93d85d4d0dcd643dbec9dae02c4f16ee914c2b7a Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 03/46] Assume Sprite._project exists always I also tried enabling the no-unnecessary-condition rule but there are a bunch of false positives relating to the "yield until something sets this variable to false" pattern. --- src/Sprite.ts | 81 ++++++++++++++------------------ src/Watcher.ts | 2 +- src/renderer/SpeechBubbleSkin.ts | 2 +- 3 files changed, 37 insertions(+), 48 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index 3fb2f7d..27f4d07 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -95,7 +95,7 @@ type InitialConditions = { }; abstract class SpriteBase { - _project: Project | null; + _project!: Project; _costumeNumber: number; _layerOrder: number; @@ -113,8 +113,7 @@ abstract class SpriteBase { _vars: Vars; constructor(initialConditions: InitialConditions, vars: Vars) { - this._project = null; - + // TODO: pass project in here, ideally const { costumeNumber, layerOrder = 0 } = initialConditions; this._costumeNumber = costumeNumber; this._layerOrder = layerOrder; @@ -139,12 +138,12 @@ abstract class SpriteBase { return this.sounds.filter((sound) => this.effectChain.isTargetOf(sound)); } - get stage(): Stage | undefined { - return this._project?.stage; + get stage(): Stage { + return this._project.stage; } - get sprites(): Partial> | undefined { - return this._project?.sprites; + get sprites(): Partial> { + return this._project.sprites; } get vars(): Vars { @@ -307,20 +306,20 @@ abstract class SpriteBase { } } - get mouse(): Mouse | undefined { - return this._project?.input.mouse; + get mouse(): Mouse { + return this._project.input.mouse; } keyPressed(name: string): boolean { - return !!this._project?.input.keyPressed(name); + return this._project.input.keyPressed(name); } - get timer(): number | undefined { - return this._project?.timer; + get timer(): number { + return this._project.timer; } restartTimer(): void { - this._project?.restartTimer(); + this._project.restartTimer(); } *startSound(soundName: string): Yielding { @@ -349,7 +348,7 @@ abstract class SpriteBase { } stopAllSounds(): void { - this._project?.stopAllSounds(); + this._project.stopAllSounds(); } stopAllOfMySounds(): void { @@ -359,15 +358,12 @@ abstract class SpriteBase { } broadcast(name: string): Promise { - // TODO: definitely assign _project then remove this - return this._project - ? this._project.fireTrigger(Trigger.BROADCAST, { name }) - : Promise.resolve(); + return this._project.fireTrigger(Trigger.BROADCAST, { name }); } *broadcastAndWait(name: string): Yielding { let running = true; - void this.broadcast(name)?.then(() => { + void this.broadcast(name).then(() => { running = false; }); @@ -377,7 +373,7 @@ abstract class SpriteBase { } clearPen(): void { - this._project?.renderer.clearPen(); + this._project.renderer.clearPen(); } *askAndWait(question: string): Yielding { @@ -386,7 +382,7 @@ abstract class SpriteBase { } let done = false; - void this._project?.askAndWait(question).then(() => { + void this._project.askAndWait(question).then(() => { done = true; }); @@ -394,11 +390,11 @@ abstract class SpriteBase { } get answer(): string | null { - return this._project?.answer ?? null; + return this._project.answer; } get loudness(): number { - return this._project?.loudness ?? -1; + return this._project.loudness; } toNumber(value) { @@ -612,7 +608,7 @@ export class Sprite extends SpriteBase { const triggers = clone.triggers.filter((tr) => tr.matches(Trigger.CLONE_START, {}, clone) ); - void this._project?._startTriggers( + void this._project._startTriggers( triggers.map((trigger) => ({ trigger, target: clone })) ); } @@ -622,11 +618,9 @@ export class Sprite extends SpriteBase { this.parent.clones = this.parent.clones.filter((clone) => clone !== this); - if (this._project) { - this._project.runningTriggers = this._project.runningTriggers.filter( - ({ target }) => target !== this - ); - } + this._project.runningTriggers = this._project.runningTriggers.filter( + ({ target }) => target !== this + ); } // TODO: is this necessary now that the clone hierarchy seems to be flattened? @@ -645,7 +639,7 @@ export class Sprite extends SpriteBase { goto(x: number, y: number): void { if (x === this.x && y === this.y) return; - if (this.penDown && this._project) { + if (this.penDown) { this._project.renderer.penLine( { x: this._x, y: this._y }, { x, y }, @@ -701,17 +695,17 @@ export class Sprite extends SpriteBase { moveAhead(value = Infinity): void { if (typeof value === "number") { - this._project?.changeSpriteLayer(this, value); + this._project.changeSpriteLayer(this, value); } else { - this._project?.changeSpriteLayer(this, 1, value); + this._project.changeSpriteLayer(this, 1, value); } } moveBehind(value = Infinity): void { if (typeof value === "number") { - this._project?.changeSpriteLayer(this, -value); + this._project.changeSpriteLayer(this, -value); } else { - this._project?.changeSpriteLayer(this, -1, value); + this._project.changeSpriteLayer(this, -1, value); } } @@ -720,7 +714,7 @@ export class Sprite extends SpriteBase { } set penDown(penDown) { - if (penDown && this._project) { + if (penDown) { this._project.renderer.penLine( { x: this.x, y: this.y }, { x: this.x, y: this.y }, @@ -746,12 +740,10 @@ export class Sprite extends SpriteBase { } stamp(): void { - this._project?.renderer.stamp(this); + this._project.renderer.stamp(this); } touching(target: "mouse" | "edge" | Sprite | Stage, fast = false): boolean { - if (!this._project || !this.mouse || !this.stage) return false; - if (typeof target === "string") { switch (target) { case "mouse": @@ -807,10 +799,10 @@ export class Sprite extends SpriteBase { if (target instanceof Color) { // "Color is touching color" - return !!this._project?.renderer.checkColorCollision(this, target, color); + return this._project.renderer.checkColorCollision(this, target, color); } else { // "Color is touching sprite" (not implemented in Scratch!) - return !!this._project?.renderer.checkSpriteCollision( + return this._project.renderer.checkSpriteCollision( this, target, false, @@ -899,11 +891,8 @@ export class Stage extends SpriteBase { } fireBackdropChanged(): Promise { - // TODO: definitely assign _project then remove this - return this._project - ? this._project.fireTrigger(Trigger.BACKDROP_CHANGED, { - backdrop: this.costume.name, - }) - : Promise.resolve(); + return this._project.fireTrigger(Trigger.BACKDROP_CHANGED, { + backdrop: this.costume.name, + }); } } diff --git a/src/Watcher.ts b/src/Watcher.ts index 4a3e158..05cf0b4 100644 --- a/src/Watcher.ts +++ b/src/Watcher.ts @@ -111,7 +111,7 @@ export default class Watcher { this._dom = { node, label, value, slider }; } - updateDOM(renderTarget: HTMLElement): void { + updateDOM(renderTarget: HTMLElement | null): void { if (renderTarget && !renderTarget.contains(this._dom.node)) { renderTarget.append(this._dom.node); } diff --git a/src/renderer/SpeechBubbleSkin.ts b/src/renderer/SpeechBubbleSkin.ts index 3da6694..e1b149b 100644 --- a/src/renderer/SpeechBubbleSkin.ts +++ b/src/renderer/SpeechBubbleSkin.ts @@ -77,7 +77,7 @@ export default class SpeechBubbleSkin extends Skin { ctx.lineTo(Math.min(x + 3 * r, x + w - r), y + h); ctx.lineTo(x + r / 2, y + h + r); ctx.lineTo(x + r, y + h); - } else if (style === "think") { + } else { ctx.ellipse(x + r * 2.25, y + h, (r * 3) / 4, r / 2, 0, 0, Math.PI); } ctx.arcTo(x, y + h, x, y, r); From 6c65e526587af47e649899d308b28addc78f1182 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 04/46] Remove ".js" suffix from imports --- src/Input.ts | 2 +- src/Loudness.ts | 2 +- src/Project.ts | 12 +++++------ src/Renderer.ts | 30 ++++++++++++++-------------- src/Sound.ts | 4 ++-- src/Sprite.ts | 20 +++++++++---------- src/Trigger.ts | 2 +- src/index.ts | 14 ++++++------- src/renderer/BitmapSkin.ts | 4 ++-- src/renderer/Drawable.ts | 14 ++++++------- src/renderer/PenSkin.ts | 10 +++++----- src/renderer/Rectangle.ts | 2 +- src/renderer/ShaderManager.ts | 6 +++--- src/renderer/Skin.ts | 2 +- src/renderer/SpeechBubbleSkin.ts | 6 +++--- src/renderer/VectorSkin.ts | 4 ++-- src/renderer/effectTransformPoint.ts | 4 ++-- 17 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/Input.ts b/src/Input.ts index 840545a..2c67b2d 100644 --- a/src/Input.ts +++ b/src/Input.ts @@ -1,4 +1,4 @@ -import type { Stage } from "./Sprite.js"; +import type { Stage } from "./Sprite"; type Mouse = { x: number; y: number; down: boolean }; diff --git a/src/Loudness.ts b/src/Loudness.ts index f780e80..77dcea8 100644 --- a/src/Loudness.ts +++ b/src/Loudness.ts @@ -1,4 +1,4 @@ -import Sound from "./Sound.js"; +import Sound from "./Sound"; const IGNORABLE_ERROR = ["NotAllowedError", "NotFoundError"]; diff --git a/src/Project.ts b/src/Project.ts index 2685383..0d6fc09 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -1,9 +1,9 @@ -import Trigger, { TriggerOptions } from "./Trigger.js"; -import Renderer from "./Renderer.js"; -import Input from "./Input.js"; -import LoudnessHandler from "./Loudness.js"; -import Sound from "./Sound.js"; -import type { Stage, Sprite } from "./Sprite.js"; +import Trigger, { TriggerOptions } from "./Trigger"; +import Renderer from "./Renderer"; +import Input from "./Input"; +import LoudnessHandler from "./Loudness"; +import Sound from "./Sound"; +import type { Stage, Sprite } from "./Sprite"; type TriggerWithTarget = { target: Sprite | Stage; diff --git a/src/Renderer.ts b/src/Renderer.ts index 98ed7b2..14162e5 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -1,18 +1,18 @@ -import Matrix, { MatrixType } from "./renderer/Matrix.js"; -import Drawable from "./renderer/Drawable.js"; -import BitmapSkin from "./renderer/BitmapSkin.js"; -import PenSkin from "./renderer/PenSkin.js"; -import SpeechBubbleSkin from "./renderer/SpeechBubbleSkin.js"; -import VectorSkin from "./renderer/VectorSkin.js"; -import Rectangle from "./renderer/Rectangle.js"; -import ShaderManager, { Shader, DrawMode } from "./renderer/ShaderManager.js"; -import { effectNames, effectBitmasks } from "./renderer/effectInfo.js"; -import type Skin from "./renderer/Skin.js"; - -import Costume from "./Costume.js"; -import type Color from "./Color.js"; -import type Project from "./Project.js"; -import { Sprite, Stage, _EffectMap, SpeechBubble } from "./Sprite.js"; +import Matrix, { MatrixType } from "./renderer/Matrix"; +import Drawable from "./renderer/Drawable"; +import BitmapSkin from "./renderer/BitmapSkin"; +import PenSkin from "./renderer/PenSkin"; +import SpeechBubbleSkin from "./renderer/SpeechBubbleSkin"; +import VectorSkin from "./renderer/VectorSkin"; +import Rectangle from "./renderer/Rectangle"; +import ShaderManager, { Shader, DrawMode } from "./renderer/ShaderManager"; +import { effectNames, effectBitmasks } from "./renderer/effectInfo"; +import type Skin from "./renderer/Skin"; + +import Costume from "./Costume"; +import type Color from "./Color"; +import type Project from "./Project"; +import { Sprite, Stage, _EffectMap, SpeechBubble } from "./Sprite"; // Rectangle used for checking collision bounds. // Rather than create a new one each time, we can just reuse this one. diff --git a/src/Sound.ts b/src/Sound.ts index b843bd1..cb6e2a8 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -1,5 +1,5 @@ -import decodeADPCMAudio, { isADPCMData } from "./lib/decode-adpcm-audio.js"; -import type { Yielding } from "./lib/yielding.js"; +import decodeADPCMAudio, { isADPCMData } from "./lib/decode-adpcm-audio"; +import type { Yielding } from "./lib/yielding"; export default class Sound { name: string; diff --git a/src/Sprite.ts b/src/Sprite.ts index 27f4d07..689c915 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -1,13 +1,13 @@ -import Color from "./Color.js"; -import Trigger from "./Trigger.js"; -import Sound, { EffectChain, AudioEffectMap } from "./Sound.js"; -import Costume from "./Costume.js"; -import type { Mouse } from "./Input.js"; -import type Project from "./Project.js"; -import type Watcher from "./Watcher.js"; -import type { Yielding } from "./lib/yielding.js"; - -import { effectNames } from "./renderer/effectInfo.js"; +import Color from "./Color"; +import Trigger from "./Trigger"; +import Sound, { EffectChain, AudioEffectMap } from "./Sound"; +import Costume from "./Costume"; +import type { Mouse } from "./Input"; +import type Project from "./Project"; +import type Watcher from "./Watcher"; +import type { Yielding } from "./lib/yielding"; + +import { effectNames } from "./renderer/effectInfo"; type Effects = { [x in typeof effectNames[number]]: number; diff --git a/src/Trigger.ts b/src/Trigger.ts index fc8a581..f4f525f 100644 --- a/src/Trigger.ts +++ b/src/Trigger.ts @@ -1,4 +1,4 @@ -import type { Sprite, Stage } from "./Sprite.js"; +import type { Sprite, Stage } from "./Sprite"; const GREEN_FLAG = Symbol("GREEN_FLAG"); const KEY_PRESSED = Symbol("KEY_PRESSED"); diff --git a/src/index.ts b/src/index.ts index 09c7210..db118dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,9 @@ -import Project from "./Project.js"; -import { Sprite, Stage } from "./Sprite.js"; -import Trigger from "./Trigger.js"; -import Watcher from "./Watcher.js"; -import Costume from "./Costume.js"; -import Color from "./Color.js"; -import Sound from "./Sound.js"; +import Project from "./Project"; +import { Sprite, Stage } from "./Sprite"; +import Trigger from "./Trigger"; +import Watcher from "./Watcher"; +import Costume from "./Costume"; +import Color from "./Color"; +import Sound from "./Sound"; export { Project, Sprite, Stage, Trigger, Watcher, Costume, Color, Sound }; diff --git a/src/renderer/BitmapSkin.ts b/src/renderer/BitmapSkin.ts index 1e26acf..e37363d 100644 --- a/src/renderer/BitmapSkin.ts +++ b/src/renderer/BitmapSkin.ts @@ -1,5 +1,5 @@ -import type Renderer from "../Renderer.js"; -import Skin from "./Skin.js"; +import type Renderer from "../Renderer"; +import Skin from "./Skin"; export default class BitmapSkin extends Skin { _image: HTMLImageElement; diff --git a/src/renderer/Drawable.ts b/src/renderer/Drawable.ts index 3af8366..c29d8d4 100644 --- a/src/renderer/Drawable.ts +++ b/src/renderer/Drawable.ts @@ -1,12 +1,12 @@ -import Matrix, { MatrixType } from "./Matrix.js"; +import Matrix, { MatrixType } from "./Matrix"; -import Rectangle from "./Rectangle.js"; -import effectTransformPoint from "./effectTransformPoint.js"; -import { effectBitmasks } from "./effectInfo.js"; -import type Skin from "./Skin.js"; +import Rectangle from "./Rectangle"; +import effectTransformPoint from "./effectTransformPoint"; +import { effectBitmasks } from "./effectInfo"; +import type Skin from "./Skin"; -import type Renderer from "../Renderer.js"; -import { Sprite, Stage } from "../Sprite.js"; +import type Renderer from "../Renderer"; +import { Sprite, Stage } from "../Sprite"; // Returns the determinant of two vectors, the vector from A to B and the vector // from A to C. If positive, it means AC is counterclockwise from AB. diff --git a/src/renderer/PenSkin.ts b/src/renderer/PenSkin.ts index 37c0c4f..b247f7b 100644 --- a/src/renderer/PenSkin.ts +++ b/src/renderer/PenSkin.ts @@ -1,8 +1,8 @@ -import Skin from "./Skin.js"; -import ShaderManager from "./ShaderManager.js"; -import type Color from "../Color.js"; -import type Renderer from "../Renderer.js"; -import type { FramebufferInfo } from "../Renderer.js"; +import Skin from "./Skin"; +import ShaderManager from "./ShaderManager"; +import type Color from "../Color"; +import type Renderer from "../Renderer"; +import type { FramebufferInfo } from "../Renderer"; export default class PenSkin extends Skin { _framebufferInfo: FramebufferInfo; diff --git a/src/renderer/Rectangle.ts b/src/renderer/Rectangle.ts index 6e2ae2b..c1f4b2f 100644 --- a/src/renderer/Rectangle.ts +++ b/src/renderer/Rectangle.ts @@ -1,4 +1,4 @@ -import type { MatrixType } from "./Matrix.js"; +import type { MatrixType } from "./Matrix"; export default class Rectangle { left: number; diff --git a/src/renderer/ShaderManager.ts b/src/renderer/ShaderManager.ts index ccfcbe2..6a20681 100644 --- a/src/renderer/ShaderManager.ts +++ b/src/renderer/ShaderManager.ts @@ -1,6 +1,6 @@ -import { SpriteShader, PenLineShader } from "./Shaders.js"; -import { effectNames, effectBitmasks } from "./effectInfo.js"; -import type Renderer from "../Renderer.js"; +import { SpriteShader, PenLineShader } from "./Shaders"; +import { effectNames, effectBitmasks } from "./effectInfo"; +import type Renderer from "../Renderer"; // Everything contained in a shader. It contains both the program, and the locations of the shader inputs. class Shader { diff --git a/src/renderer/Skin.ts b/src/renderer/Skin.ts index 6938c1d..53164bb 100644 --- a/src/renderer/Skin.ts +++ b/src/renderer/Skin.ts @@ -1,4 +1,4 @@ -import type Renderer from "../Renderer.js"; +import type Renderer from "../Renderer"; export default abstract class Skin { renderer: Renderer; diff --git a/src/renderer/SpeechBubbleSkin.ts b/src/renderer/SpeechBubbleSkin.ts index e1b149b..5e886a7 100644 --- a/src/renderer/SpeechBubbleSkin.ts +++ b/src/renderer/SpeechBubbleSkin.ts @@ -1,6 +1,6 @@ -import Skin from "./Skin.js"; -import type Renderer from "../Renderer.js"; -import type { SpeechBubble } from "../Sprite.js"; +import Skin from "./Skin"; +import type Renderer from "../Renderer"; +import type { SpeechBubble } from "../Sprite"; const bubbleStyle = { maxLineWidth: 170, diff --git a/src/renderer/VectorSkin.ts b/src/renderer/VectorSkin.ts index 4ce9c33..55bfd86 100644 --- a/src/renderer/VectorSkin.ts +++ b/src/renderer/VectorSkin.ts @@ -1,5 +1,5 @@ -import Skin from "./Skin.js"; -import type Renderer from "../Renderer.js"; +import Skin from "./Skin"; +import type Renderer from "../Renderer"; // This means that the smallest mipmap will be 1/(2**4)th the size of the sprite's "100%" size. const MIPMAP_OFFSET = 4; diff --git a/src/renderer/effectTransformPoint.ts b/src/renderer/effectTransformPoint.ts index 942d797..c3f058f 100644 --- a/src/renderer/effectTransformPoint.ts +++ b/src/renderer/effectTransformPoint.ts @@ -1,5 +1,5 @@ -import { effectBitmasks } from "./effectInfo.js"; -import type Drawable from "./Drawable.js"; +import { effectBitmasks } from "./effectInfo"; +import type Drawable from "./Drawable"; const CENTER = 0.5; const EPSILON = 1e-3; From 96e0618f9ced2d681b98245f03e501cede4a386d Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 05/46] Add visibility modifiers to all class members These don't exactly match which properties are and are not underscored-- that's gonna have to be a future project --- .eslintrc.json | 1 + src/Color.ts | 56 ++++---- src/Costume.ts | 19 +-- src/Input.ts | 28 ++-- src/Loudness.ts | 21 ++- src/Project.ts | 57 ++++---- src/Renderer.ts | 92 ++++++------- src/Sound.ts | 108 ++++++++-------- src/Sprite.ts | 214 ++++++++++++++++--------------- src/Trigger.ts | 46 ++++--- src/Watcher.ts | 78 +++++------ src/renderer/BitmapSkin.ts | 14 +- src/renderer/Drawable.ts | 75 +++++------ src/renderer/Matrix.ts | 16 ++- src/renderer/PenSkin.ts | 14 +- src/renderer/Rectangle.ts | 35 ++--- src/renderer/ShaderManager.ts | 24 ++-- src/renderer/Skin.ts | 20 +-- src/renderer/SpeechBubbleSkin.ts | 32 ++--- src/renderer/VectorSkin.ts | 30 ++--- 20 files changed, 502 insertions(+), 478 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e22b24b..6c8e9d3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,6 +31,7 @@ }, "rules": { "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/explicit-member-accessibility": "error", "@typescript-eslint/prefer-return-this-type": "error", "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}] } diff --git a/src/Color.ts b/src/Color.ts index c8aceb1..11eff0f 100644 --- a/src/Color.ts +++ b/src/Color.ts @@ -91,28 +91,28 @@ function hsvToRGB( } export default class Color { - _h = 0; - _s = 0; - _v = 0; - _a = 1; + private _h = 0; + private _s = 0; + private _v = 0; + private _a = 1; - constructor(h = 0, s = 0, v = 0, a = 1) { + public constructor(h = 0, s = 0, v = 0, a = 1) { this.h = h; this.s = s; this.v = v; this.a = a; } - static rgb(r: number, g: number, b: number, a = 1): Color { + public static rgb(r: number, g: number, b: number, a = 1): Color { const { h, s, v } = rgbToHSV(r, g, b); return new Color(h, s, v, a); } - static hsv(h: number, s: number, v: number, a = 1): Color { + public static hsv(h: number, s: number, v: number, a = 1): Color { return new Color(h, s, v, a); } - static num(n: number | string): Color { + public static num(n: number | string): Color { n = Number(n); // Match Scratch rgba system @@ -125,62 +125,62 @@ export default class Color { } // Red - get r(): number { + public get r(): number { return hsvToRGB(this.h, this.s, this.v).r; } - set r(r) { + public set r(r) { this._setRGB(r, this.g, this.b); } // Green - get g(): number { + public get g(): number { return hsvToRGB(this.h, this.s, this.v).g; } - set g(g) { + public set g(g) { this._setRGB(this.r, g, this.b); } // Blue - get b(): number { + public get b(): number { return hsvToRGB(this.h, this.s, this.v).b; } - set b(b) { + public set b(b) { this._setRGB(this.r, this.g, b); } // Alpha - get a(): number { + public get a(): number { return this._a; } - set a(a) { + public set a(a) { this._a = clamp(a, 0, 1); } // Hue - get h(): number { + public get h(): number { return this._h; } - set h(h) { + public set h(h) { this._h = ((h % 100) + 100) % 100; } // Shade - get s(): number { + public get s(): number { return this._s; } - set s(s) { + public set s(s) { this._s = clamp(s, 0, 100); } // Value - get v(): number { + public get v(): number { return this._v; } - set v(v) { + public set v(v) { this._v = clamp(v, 0, 100); } - _setRGB(r: number, g: number, b: number): void { + private _setRGB(r: number, g: number, b: number): void { r = clamp(r, 0, 255); g = clamp(g, 0, 255); b = clamp(b, 0, 255); @@ -192,7 +192,7 @@ export default class Color { this.v = v; } - toHexString(forceIncludeAlpha = false): string { + public toHexString(forceIncludeAlpha = false): string { const toHexDigits = (n: number): string => { n = clamp(Math.round(n), 0, 255); @@ -212,7 +212,7 @@ export default class Color { return hex; } - toRGBString(forceIncludeAlpha = false): string { + public toRGBString(forceIncludeAlpha = false): string { const rgb = [this.r, this.g, this.b].map(Math.round); if (forceIncludeAlpha || this.a !== 1) { @@ -221,17 +221,17 @@ export default class Color { return `rgb(${rgb.join(", ")})`; } - toRGBA(): [number, number, number, number] { + public toRGBA(): [number, number, number, number] { const rgb = hsvToRGB(this._h, this._s, this._v); return [rgb.r, rgb.g, rgb.b, this._a * 255]; } - toRGBANormalized(): [number, number, number, number] { + public toRGBANormalized(): [number, number, number, number] { const rgb = hsvToRGB(this._h, this._s, this._v); return [rgb.r / 255, rgb.g / 255, rgb.b / 255, this._a]; } - toString(): string { + public toString(): string { return this.toRGBString(); } } diff --git a/src/Costume.ts b/src/Costume.ts index e312c44..45bca04 100644 --- a/src/Costume.ts +++ b/src/Costume.ts @@ -1,11 +1,12 @@ export default class Costume { - name: string; - url: string; - img: HTMLImageElement; - isBitmap: boolean; - resolution: 2 | 1; - center: { x: number; y: number }; - constructor(name: string, url: string, center = { x: 0, y: 0 }) { + public name: string; + public url: string; + public img: HTMLImageElement; + public isBitmap: boolean; + public resolution: 2 | 1; + public center: { x: number; y: number }; + + public constructor(name: string, url: string, center = { x: 0, y: 0 }) { this.name = name; this.url = url; @@ -20,11 +21,11 @@ export default class Costume { this.center = center; } - get width(): number { + public get width(): number { return this.img.naturalWidth; } - get height(): number { + public get height(): number { return this.img.naturalHeight; } } diff --git a/src/Input.ts b/src/Input.ts index 2c67b2d..4bb53fa 100644 --- a/src/Input.ts +++ b/src/Input.ts @@ -3,13 +3,13 @@ import type { Stage } from "./Sprite"; type Mouse = { x: number; y: number; down: boolean }; export default class Input { - _stage: Stage; - _canvas: HTMLCanvasElement; - _onKeyDown: (key: string) => unknown; + private _stage: Stage; + private _canvas: HTMLCanvasElement; + private _onKeyDown: (key: string) => unknown; - mouse: Mouse; - keys: string[]; - constructor( + public mouse: Mouse; + public keys: string[]; + public constructor( stage: Input["_stage"], canvas: Input["_canvas"], onKeyDown: Input["_onKeyDown"] @@ -34,7 +34,7 @@ export default class Input { this._onKeyDown = onKeyDown; } - _mouseMove(e: MouseEvent): void { + private _mouseMove(e: MouseEvent): void { const rect = this._canvas.getBoundingClientRect(); const scaleX = this._stage.width / rect.width; const scaleY = this._stage.height / rect.height; @@ -50,26 +50,26 @@ export default class Input { }; } - _mouseDown(): void { + private _mouseDown(): void { this.mouse = { ...this.mouse, down: true, }; } - _mouseUp(): void { + private _mouseUp(): void { this.mouse = { ...this.mouse, down: false, }; } - _keyup(e: KeyboardEvent): void { + private _keyup(e: KeyboardEvent): void { const key = this._getKeyName(e); this.keys = this.keys.filter((k) => k !== key); } - _keydown(e: KeyboardEvent): void { + private _keydown(e: KeyboardEvent): void { e.preventDefault(); const key = this._getKeyName(e); @@ -80,7 +80,7 @@ export default class Input { this._onKeyDown(key); } - _getKeyName(e: KeyboardEvent): string { + private _getKeyName(e: KeyboardEvent): string { if (e.key === "ArrowUp") return "up arrow"; if (e.key === "ArrowDown") return "down arrow"; if (e.key === "ArrowLeft") return "left arrow"; @@ -91,12 +91,12 @@ export default class Input { return e.key.toLowerCase(); } - keyPressed(name: string): boolean { + public keyPressed(name: string): boolean { if (name === "any") return this.keys.length > 0; return this.keys.indexOf(name) > -1; } - focus(): void { + public focus(): void { this._canvas.focus(); } } diff --git a/src/Loudness.ts b/src/Loudness.ts index 77dcea8..c9a6c20 100644 --- a/src/Loudness.ts +++ b/src/Loudness.ts @@ -15,22 +15,21 @@ const enum ConnectionState { // https://github.com/LLK/scratch-audio/blob/develop/src/Loudness.js export default class LoudnessHandler { - connectionState: ConnectionState; - audioStream: MediaStream | undefined; - analyser: AnalyserNode | undefined; - micDataArray: Float32Array | undefined; - _lastValue: number | undefined; + private connectionState: ConnectionState; + private audioStream: MediaStream | undefined; + private analyser: AnalyserNode | undefined; + private micDataArray: Float32Array | undefined; + private _lastValue: number | undefined; - constructor() { - // TODO: use a TypeScript enum + public constructor() { this.connectionState = ConnectionState.NOT_CONNECTED; } - get audioContext(): AudioContext { + private get audioContext(): AudioContext { return Sound.audioContext; } - async connect(): Promise { + private async connect(): Promise { // If we're in the middle of connecting, or failed to connect, // don't attempt to connect again if (this.connectionState !== ConnectionState.NOT_CONNECTED) return; @@ -57,7 +56,7 @@ export default class LoudnessHandler { } } - get loudness(): number { + private get loudness(): number { if ( this.connectionState !== ConnectionState.CONNECTED || !this.audioStream?.active || @@ -88,7 +87,7 @@ export default class LoudnessHandler { return rms; } - getLoudness(): number { + public getLoudness(): number { void this.connect(); return this.loudness; } diff --git a/src/Project.ts b/src/Project.ts index 0d6fc09..44a4505 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -11,20 +11,20 @@ type TriggerWithTarget = { }; export default class Project { - stage: Stage; - sprites: Partial>; - renderer: Renderer; - input: Input; + public stage: Stage; + public sprites: Partial>; + public renderer: Renderer; + public input: Input; - loudnessHandler: LoudnessHandler; - _cachedLoudness: number | null; + private loudnessHandler: LoudnessHandler; + private _cachedLoudness: number | null; - runningTriggers: TriggerWithTarget[]; - _prevStepTriggerPredicates: WeakMap; - answer: string | null; - timerStart!: Date; + public runningTriggers: TriggerWithTarget[]; + private _prevStepTriggerPredicates: WeakMap; + public answer: string | null; + private timerStart!: Date; - constructor(stage: Stage, sprites = {}, { frameRate = 30 } = {}) { + public constructor(stage: Stage, sprites = {}, { frameRate = 30 } = {}) { this.stage = stage; this.sprites = sprites; @@ -62,7 +62,7 @@ export default class Project { this._renderLoop(); } - attach(renderTarget: string | HTMLElement): void { + public attach(renderTarget: string | HTMLElement): void { this.renderer.setRenderTarget(renderTarget); this.renderer.stage.addEventListener("click", () => { // Chrome requires a user gesture on the page before we can start the @@ -92,7 +92,7 @@ export default class Project { }); } - greenFlag(): void { + public greenFlag(): void { // Chrome requires a user gesture on the page before we can start the // audio context. // When greenFlag is triggered, it's likely that the cause of it was some @@ -105,7 +105,7 @@ export default class Project { } // Find triggers which match the given condition - _matchingTriggers( + private _matchingTriggers( triggerMatches: (tr: Trigger, target: Sprite | Stage) => boolean ): TriggerWithTarget[] { const matchingTriggers = []; @@ -121,7 +121,7 @@ export default class Project { return matchingTriggers; } - _stepEdgeActivatedTriggers(): void { + private _stepEdgeActivatedTriggers(): void { const edgeActivated = this._matchingTriggers((tr) => tr.isEdgeActivated); const triggersToStart = []; for (const triggerWithTarget of edgeActivated) { @@ -152,7 +152,7 @@ export default class Project { void this._startTriggers(triggersToStart); } - step(): void { + private step(): void { this._cachedLoudness = null; this._stepEdgeActivatedTriggers(); @@ -168,7 +168,7 @@ export default class Project { ); } - render(): void { + private render(): void { // Render to canvas this.renderer.update(); @@ -182,12 +182,12 @@ export default class Project { } } - _renderLoop(): void { + private _renderLoop(): void { requestAnimationFrame(this._renderLoop.bind(this)); this.render(); } - fireTrigger(trigger: symbol, options?: TriggerOptions): Promise { + public fireTrigger(trigger: symbol, options?: TriggerOptions): Promise { // Special trigger behaviors if (trigger === Trigger.GREEN_FLAG) { this.restartTimer(); @@ -212,7 +212,8 @@ export default class Project { return this._startTriggers(matchingTriggers); } - _startTriggers(triggers: TriggerWithTarget[]): Promise { + // TODO: add a way to start clone triggers from fireTrigger then make this private + public _startTriggers(triggers: TriggerWithTarget[]): Promise { // Only add these triggers to this.runningTriggers if they're not already there. // TODO: if the triggers are already running, they'll be restarted but their execution order is unchanged. // Does that match Scratch's behavior? @@ -234,17 +235,17 @@ export default class Project { ).then(); } - get spritesAndClones(): Sprite[] { + public get spritesAndClones(): Sprite[] { return Object.values(this.sprites) .flatMap((sprite) => sprite!.andClones()) .sort((a, b) => a._layerOrder - b._layerOrder); } - get spritesAndStage(): [...Sprite[], Stage] { + public get spritesAndStage(): [...Sprite[], Stage] { return [...this.spritesAndClones, this.stage]; } - changeSpriteLayer( + public changeSpriteLayer( sprite: Sprite, layerDelta: number, relativeToSprite = sprite @@ -270,26 +271,26 @@ export default class Project { }); } - stopAllSounds(): void { + public stopAllSounds(): void { for (const target of this.spritesAndStage) { target.stopAllOfMySounds(); } } - get timer(): number { + public get timer(): number { const ms = new Date().getTime() - this.timerStart.getTime(); return ms / 1000; } - restartTimer(): void { + public restartTimer(): void { this.timerStart = new Date(); } - async askAndWait(question: string): Promise { + public async askAndWait(question: string): Promise { this.answer = await this.renderer.displayAskBox(question); } - get loudness(): number { + public get loudness(): number { if (this._cachedLoudness === null) { this._cachedLoudness = this.loudnessHandler.getLoudness(); } diff --git a/src/Renderer.ts b/src/Renderer.ts index 14162e5..6eda197 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -49,22 +49,25 @@ export type FramebufferInfo = { }; export default class Renderer { - project: Project; - stage: HTMLCanvasElement; - gl: WebGLRenderingContext; - renderTarget: HTMLElement | null = null; - - _shaderManager: ShaderManager; - _drawables: WeakMap; - _skins: WeakMap; - - _currentShader: Shader | null; - _currentFramebuffer: WebGLFramebuffer | null; - _screenSpaceScale: number; - _penSkin: PenSkin; - _collisionBuffer: FramebufferInfo; - - constructor(project: Project, renderTarget: HTMLElement | string | null) { + public project: Project; + public stage: HTMLCanvasElement; + public gl: WebGLRenderingContext; + public renderTarget: HTMLElement | null = null; + + public _shaderManager: ShaderManager; + private _drawables: WeakMap; + private _skins: WeakMap; + + private _currentShader: Shader | null; + private _currentFramebuffer: WebGLFramebuffer | null; + private _screenSpaceScale: number; + private _penSkin: PenSkin; + private _collisionBuffer: FramebufferInfo; + + public constructor( + project: Project, + renderTarget: HTMLElement | string | null + ) { const w = project.stage.width; const h = project.stage.height; this.project = project; @@ -122,7 +125,7 @@ export default class Renderer { } // Retrieve a given object (e.g. costume or speech bubble)'s skin. If it doesn't exist, make one. - _getSkin(obj: object): Skin { + public _getSkin(obj: object): Skin { const existingSkin = this._skins.get(obj); if (existingSkin) return existingSkin; @@ -143,7 +146,7 @@ export default class Renderer { } // Retrieve the renderer-specific data object for a given sprite or clone. If it doesn't exist, make one. - _getDrawable(sprite: Sprite | Stage): Drawable { + public _getDrawable(sprite: Sprite | Stage): Drawable { const existingDrawable = this._drawables.get(sprite); if (existingDrawable) return existingDrawable; @@ -156,7 +159,7 @@ export default class Renderer { // * The framebuffer itself. // * The texture backing the framebuffer. // * The resolution (width and height) of the framebuffer. - _createFramebufferInfo( + public _createFramebufferInfo( width: number, height: number, filtering: @@ -222,7 +225,7 @@ export default class Renderer { return framebufferInfo; } - _setShader(shader: Shader): boolean { + public _setShader(shader: Shader): boolean { if (shader !== this._currentShader) { const gl = this.gl; gl.useProgram(shader.program); @@ -251,7 +254,7 @@ export default class Renderer { return false; } - _setFramebuffer(framebufferInfo: FramebufferInfo | null): void { + public _setFramebuffer(framebufferInfo: FramebufferInfo | null): void { if (framebufferInfo !== this._currentFramebuffer) { this._currentFramebuffer = framebufferInfo; if (framebufferInfo === null) { @@ -269,7 +272,7 @@ export default class Renderer { } } - setRenderTarget(renderTarget: HTMLElement | string | null): void { + public setRenderTarget(renderTarget: HTMLElement | string | null): void { if (typeof renderTarget === "string") { renderTarget = document.querySelector(renderTarget) as HTMLElement; } @@ -283,7 +286,7 @@ export default class Renderer { } // Handles rendering of all layers (including stage, pen layer, sprites, and all clones) in proper order. - _renderLayers( + private _renderLayers( layers?: Set, optionsIn: { filter?: (layer: Sprite | Stage | PenSkin) => boolean; @@ -338,7 +341,7 @@ export default class Renderer { } } - _updateStageSize(): void { + private _updateStageSize(): void { if (this._currentShader) { // The shader is passed things in "Scratch-space" (-240, 240) and (-180, 180). // This tells it those dimensions so it can convert them to OpenGL "clip-space" (-1, 1). @@ -360,7 +363,7 @@ export default class Renderer { } // Keep the canvas size in sync with the CSS size. - _resize(): void { + private _resize(): void { const stageSize = this.stage.getBoundingClientRect(); const ratio = window.devicePixelRatio; const adjustedWidth = Math.round(stageSize.width * ratio); @@ -380,7 +383,7 @@ export default class Renderer { } } - update(): void { + public update(): void { this._resize(); // Draw to the screen, not to a framebuffer. @@ -394,7 +397,7 @@ export default class Renderer { this._renderLayers(); } - createStage(w: number, h: number): HTMLCanvasElement { + public createStage(w: number, h: number): HTMLCanvasElement { const stage = document.createElement("canvas"); stage.width = w; stage.height = h; @@ -414,7 +417,7 @@ export default class Renderer { } // Calculate the transform matrix for a speech bubble attached to a sprite. - _calculateSpeechBubbleMatrix( + private _calculateSpeechBubbleMatrix( spr: Sprite, speechBubbleSkin: SpeechBubbleSkin ): MatrixType { @@ -440,7 +443,7 @@ export default class Renderer { return m; } - _renderSkin( + private _renderSkin( skin: Skin, drawMode: DrawMode, matrix: MatrixType, @@ -464,13 +467,13 @@ export default class Renderer { if (effectBitmask !== 0 && effects) { for (const effect of effectNames) { - const effectVal = effects._effectValues[effect]; + const effectVal = effects[effect]; if (effectVal !== 0) gl.uniform1f(shader.uniforms[`u_${effect}`], effectVal); } // Pixelate effect needs the skin size - if (effects._effectValues.pixelate !== 0) + if (effects.pixelate !== 0) gl.uniform2f( shader.uniforms.u_skinSize, skin.width ?? 0, @@ -500,7 +503,10 @@ export default class Renderer { this.gl.drawArrays(this.gl.TRIANGLES, 0, 6); } - renderSprite(sprite: Sprite | Stage, options: RenderSpriteOptions): void { + private renderSprite( + sprite: Sprite | Stage, + options: RenderSpriteOptions + ): void { const spriteScale = "size" in sprite ? sprite.size / 100 : 1; this._renderSkin( @@ -533,16 +539,16 @@ export default class Renderer { } } - getTightBoundingBox(sprite: Sprite | Stage): Rectangle { + public getTightBoundingBox(sprite: Sprite | Stage): Rectangle { return this._getDrawable(sprite).getTightBoundingBox(); } - getBoundingBox(sprite: Sprite | Stage): Rectangle { + public getBoundingBox(sprite: Sprite | Stage): Rectangle { return Rectangle.fromMatrix(this._getDrawable(sprite).getMatrix()); } // Mask drawing in to only areas where this sprite is opaque. - _stencilSprite(spr: Sprite | Stage, colorMask?: Color): void { + private _stencilSprite(spr: Sprite | Stage, colorMask?: Color): void { const gl = this.gl; gl.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT | gl.STENCIL_BUFFER_BIT); @@ -592,7 +598,7 @@ export default class Renderer { gl.colorMask(true, true, true, true); } - checkSpriteCollision( + public checkSpriteCollision( spr: Sprite | Stage, targets: Set | (Sprite | Stage)[] | Sprite | Stage, fast?: boolean, @@ -676,7 +682,7 @@ export default class Renderer { return false; } - checkColorCollision( + public checkColorCollision( spr: Sprite | Stage, targetsColor: Color, sprColor?: Color @@ -738,7 +744,7 @@ export default class Renderer { } // Pick the topmost sprite at the given point (if one exists). - pick( + public pick( sprites: (Sprite | Stage)[], point: { x: number; y: number } ): Sprite | Stage | null { @@ -777,7 +783,7 @@ export default class Renderer { return sprites[index]; } - checkPointCollision( + public checkPointCollision( spr: Sprite, point: { x: number; y: number }, fast?: boolean @@ -811,7 +817,7 @@ export default class Renderer { return hoveredPixel[3] !== 0; } - penLine( + public penLine( pt1: { x: number; y: number }, pt2: { x: number; y: number }, color: Color, @@ -820,16 +826,16 @@ export default class Renderer { this._penSkin.penLine(pt1, pt2, color, size); } - clearPen(): void { + public clearPen(): void { this._penSkin.clear(); } - stamp(spr: Sprite | Stage): void { + public stamp(spr: Sprite | Stage): void { this._setFramebuffer(this._penSkin._framebufferInfo); this._renderLayers(new Set([spr]), { renderSpeechBubbles: false }); } - displayAskBox(question: string): Promise { + public displayAskBox(question: string): Promise { if (!this.renderTarget) return Promise.resolve(""); const askBox = document.createElement("form"); askBox.classList.add("leopard__askBox"); diff --git a/src/Sound.ts b/src/Sound.ts index cb6e2a8..8dcab93 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -2,19 +2,19 @@ import decodeADPCMAudio, { isADPCMData } from "./lib/decode-adpcm-audio"; import type { Yielding } from "./lib/yielding"; export default class Sound { - name: string; - url: string; + public name: string; + public url: string; - audioBuffer: AudioBuffer | null; - source: AudioBufferSourceNode | null; - playbackRate: number; - target?: AudioNode; + private audioBuffer: AudioBuffer | null; + private source: AudioBufferSourceNode | null; + private playbackRate: number; + private target?: AudioNode; - _markDone?: () => void; - _doneDownloading?: (fromMoreRecentCall: boolean) => void; + private _markDone?: () => void; + private _doneDownloading?: (fromMoreRecentCall: boolean) => void; - static _audioContext: AudioContext | undefined; - constructor(name: string, url: string) { + private static _audioContext: AudioContext | undefined; + public constructor(name: string, url: string) { this.name = name; this.url = url; @@ -26,11 +26,11 @@ export default class Sound { void this.downloadMyAudioBuffer(); } - get duration(): number { + public get duration(): number { return this.audioBuffer ? this.audioBuffer.duration : 0; } - *start(): Yielding { + public *start(): Yielding { let started = false; let isLatestCallToStart = true; @@ -83,7 +83,7 @@ export default class Sound { return isLatestCallToStart; } - *playUntilDone(): Yielding { + public *playUntilDone(): Yielding { let playing = true; const isLatestCallToStart = yield* this.start(); @@ -118,7 +118,7 @@ export default class Sound { while (playing) yield; } - stop(): void { + public stop(): void { if (this._markDone) { this._markDone(); } @@ -129,7 +129,7 @@ export default class Sound { } } - downloadMyAudioBuffer(): Promise { + public downloadMyAudioBuffer(): Promise { return fetch(this.url) .then((body) => body.arrayBuffer()) .then((arrayBuffer) => { @@ -162,7 +162,7 @@ export default class Sound { }); } - playMyAudioBuffer(): void { + private playMyAudioBuffer(): void { if (!this.audioBuffer) { return; } @@ -182,7 +182,7 @@ export default class Sound { this.source.start(Sound.audioContext.currentTime); } - connect(target: AudioNode): void { + public connect(target: AudioNode): void { if (target !== this.target) { this.target = target; if (this.source) { @@ -192,20 +192,20 @@ export default class Sound { } } - setPlaybackRate(value: number): void { + public setPlaybackRate(value: number): void { this.playbackRate = value; if (this.source) { this.source.playbackRate.value = value; } } - isConnectedTo(target: AudioNode): boolean { + public isConnectedTo(target: AudioNode): boolean { return this.target === target; } // Note: "this" refers to the Sound class in static functions. - static get audioContext(): AudioContext { + public static get audioContext(): AudioContext { if (!this._audioContext) { const AudioContext = window.AudioContext || @@ -215,10 +215,6 @@ export default class Sound { } return this._audioContext; } - - static decodeADPCMAudio(audioBuffer: ArrayBuffer): Promise { - return decodeADPCMAudio(audioBuffer, this.audioContext); - } } // Instead of creating a basic Effect class and then implementing a subclass @@ -334,20 +330,20 @@ export class EffectChain { // it affects. // TODO: stop storing config; we just use getNonPatchSoundList directly - config: EffectChainConfig; - inputNode: AudioNode; - getNonPatchSoundList: () => Sound[]; - effectValues!: Record; - effectNodes: { + private config: EffectChainConfig; + public inputNode: AudioNode; + private getNonPatchSoundList: () => Sound[]; + private effectValues!: Record; + private effectNodes: { [T in typeof effectDescriptors[number] as T["name"]]?: ReturnType< // We need to infer this type here, I think // eslint-disable-next-line @typescript-eslint/no-unused-vars T extends PatchDescriptor ? T["makeNodes"] : never >; }; - target?: AudioNode; + private target?: AudioNode; - constructor(config: EffectChainConfig) { + public constructor(config: EffectChainConfig) { const { getNonPatchSoundList } = config; this.config = config; @@ -368,7 +364,7 @@ export class EffectChain { this.getNonPatchSoundList = getNonPatchSoundList; } - resetToInitial(): void { + public resetToInitial(): void { // Note: some effects won't be reset by this function, except for when they // are set for the first time (i.e. when the EffectChain is instantiated). // Look for the "reset: false" flag in the effect descriptor list. @@ -387,7 +383,7 @@ export class EffectChain { } } - updateAudioEffect(name: EffectName): void { + private updateAudioEffect(name: EffectName): void { const descriptor = EffectChain.getEffectDescriptor(name); if (!descriptor) { @@ -527,7 +523,7 @@ export class EffectChain { } } - connect(target: AudioNode): void { + public connect(target: AudioNode): void { this.target = target; // All the code here is basically the same as what's written in @@ -554,7 +550,10 @@ export class EffectChain { last.output.connect(target); } - setEffectValue(name: EffectName, value: number | string | boolean): void { + public setEffectValue( + name: EffectName, + value: number | string | boolean + ): void { value = Number(value); if ( name in this.effectValues && @@ -567,7 +566,10 @@ export class EffectChain { } } - changeEffectValue(name: EffectName, value: number | string | boolean): void { + private changeEffectValue( + name: EffectName, + value: number | string | boolean + ): void { value = Number(value); if (name in this.effectValues && !isNaN(value) && value !== 0) { this.effectValues[name] += value; @@ -576,7 +578,7 @@ export class EffectChain { } } - clampEffectValue(name: EffectName): void { + private clampEffectValue(name: EffectName): void { // Not all effects are clamped (pitch, for example); it's also possible to // specify only a minimum or maximum bound, instead of both. const descriptor = EffectChain.getEffectDescriptor(name); @@ -592,11 +594,11 @@ export class EffectChain { this.effectValues[name] = value; } - getEffectValue(name: EffectName): number { + public getEffectValue(name: EffectName): number { return this.effectValues[name] || 0; } - clone(newConfig: EffectChainConfig): EffectChain { + public clone(newConfig: EffectChainConfig): EffectChain { const newEffectChain = new EffectChain( Object.assign({}, this.config, newConfig) ); @@ -616,7 +618,7 @@ export class EffectChain { return newEffectChain; } - applyToSound(sound: Sound): void { + public applyToSound(sound: Sound): void { sound.connect(this.inputNode); for (const [name, value] of Object.entries(this.effectValues) as [ @@ -630,11 +632,11 @@ export class EffectChain { } } - isTargetOf(sound: Sound): boolean { + public isTargetOf(sound: Sound): boolean { return sound.isConnectedTo(this.inputNode); } - static getInitialEffectValues(): Record { + private static getInitialEffectValues(): Record { // This would be an excellent place to use Object.fromEntries, but that // function has been implemented in only the latest of a few modern // browsers. :P @@ -645,7 +647,7 @@ export class EffectChain { return initials as Record; } - static getEffectDescriptor( + private static getEffectDescriptor( name: EffectName ): typeof EffectChain["effectDescriptors"][number] { // We know this is non-null because this.effectDescriptors has every effect descriptor in it. @@ -655,15 +657,15 @@ export class EffectChain { )!; } - static getFirstEffectDescriptor(): typeof effectDescriptors[number] { + private static getFirstEffectDescriptor(): typeof effectDescriptors[number] { return this.effectDescriptors[0]; } - static getLastEffectDescriptor(): typeof effectDescriptors[number] { + private static getLastEffectDescriptor(): typeof effectDescriptors[number] { return this.effectDescriptors[this.effectDescriptors.length - 1]; } - static getNextEffectDescriptor( + private static getNextEffectDescriptor( name: EffectName ): typeof effectDescriptors[number] | undefined { // .find() provides three values to its passed function: the value of the @@ -680,7 +682,7 @@ export class EffectChain { .find((_, i) => this.effectDescriptors[i].name === name); } - static getPreviousEffectDescriptor( + private static getPreviousEffectDescriptor( name: EffectName ): typeof effectDescriptors[number] | undefined { // This function's a little simpler, since it doesn't involve shifting the @@ -697,10 +699,10 @@ export class EffectChain { // These are constant values which can be affected to tweak the way effects // are applied. They match the values used in Scratch 3.0. - static decayDuration = 0.025; - static decayWait = 0.05; + public static decayDuration = 0.025; + public static decayWait = 0.05; - static effectDescriptors = effectDescriptors; + public static effectDescriptors = effectDescriptors; } type EffectDescriptorBase = { @@ -740,9 +742,9 @@ export class AudioEffectMap { // for graphic effects). It takes an EffectChain and automatically generates // properties according to the names of the effect descriptors, acting with // the EffectChain's API when accessed. - effectChain: EffectChain; + private effectChain: EffectChain; - constructor(effectChain: EffectChain) { + public constructor(effectChain: EffectChain) { this.effectChain = effectChain; for (const { name } of EffectChain.effectDescriptors) { @@ -754,7 +756,7 @@ export class AudioEffectMap { } } - clear(): void { + public clear(): void { this.effectChain.resetToInitial(); } } diff --git a/src/Sprite.ts b/src/Sprite.ts index 689c915..d2a19af 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -17,18 +17,18 @@ type Effects = { // By setting an effect, the bitmask is updated as well. // This allows the bitmask to be used to uniquely identify a set of enabled effects. export class _EffectMap implements Effects { - _bitmask: number; - _effectValues: Record; + public _bitmask: number; + private _effectValues: Record; // TODO: TypeScript can't automatically infer these - color!: number; - fisheye!: number; - whirl!: number; - pixelate!: number; - mosaic!: number; - brightness!: number; - ghost!: number; - - constructor() { + public color!: number; + public fisheye!: number; + public whirl!: number; + public pixelate!: number; + public mosaic!: number; + public brightness!: number; + public ghost!: number; + + public constructor() { this._bitmask = 0; this._effectValues = { color: 0, @@ -63,7 +63,7 @@ export class _EffectMap implements Effects { } } - _clone(): _EffectMap { + public _clone(): _EffectMap { const m = new _EffectMap(); for (const effectName of Object.keys( this._effectValues @@ -73,7 +73,7 @@ export class _EffectMap implements Effects { return m; } - clear(): void { + public clear(): void { for (const effectName of Object.keys( this._effectValues ) as (keyof typeof this._effectValues)[]) { @@ -95,24 +95,26 @@ type InitialConditions = { }; abstract class SpriteBase { - _project!: Project; + // TODO: make private + public _project!: Project; - _costumeNumber: number; - _layerOrder: number; - triggers: Trigger[]; - watchers: Partial>; - costumes: Costume[]; - sounds: Sound[]; + protected _costumeNumber: number; + // TODO: make private + public _layerOrder: number; + public triggers: Trigger[]; + public watchers: Partial>; + protected costumes: Costume[]; + protected sounds: Sound[]; - effectChain: EffectChain; - effects: _EffectMap; - audioEffects: AudioEffectMap; + protected effectChain: EffectChain; + public effects: _EffectMap; + public audioEffects: AudioEffectMap; - _speechBubble?: SpeechBubble; + public _speechBubble?: SpeechBubble; - _vars: Vars; + protected _vars: Vars; - constructor(initialConditions: InitialConditions, vars: Vars) { + public constructor(initialConditions: InitialConditions, vars: Vars) { // TODO: pass project in here, ideally const { costumeNumber, layerOrder = 0 } = initialConditions; this._costumeNumber = costumeNumber; @@ -134,31 +136,31 @@ abstract class SpriteBase { this._vars = vars ?? {}; } - getSoundsPlayedByMe(): Sound[] { + protected getSoundsPlayedByMe(): Sound[] { return this.sounds.filter((sound) => this.effectChain.isTargetOf(sound)); } - get stage(): Stage { + public get stage(): Stage { return this._project.stage; } - get sprites(): Partial> { + public get sprites(): Partial> { return this._project.sprites; } - get vars(): Vars { + public get vars(): Vars { return this._vars; } - get costumeNumber(): number { + public get costumeNumber(): number { return this._costumeNumber; } - set costumeNumber(number) { + public set costumeNumber(number) { this._costumeNumber = this.wrapClamp(number, 1, this.costumes.length); } - set costume(costume: number | string | Costume) { + public set costume(costume: number | string | Costume) { if (costume instanceof Costume) { const costumeIndex = this.costumes.indexOf(costume); if (costumeIndex > -1) { @@ -218,31 +220,31 @@ abstract class SpriteBase { } } - get costume(): Costume { + public get costume(): Costume { return this.costumes[this.costumeNumber - 1]; } - degToRad(deg: number): number { + public degToRad(deg: number): number { return (deg * Math.PI) / 180; } - radToDeg(rad: number): number { + public radToDeg(rad: number): number { return (rad * 180) / Math.PI; } - degToScratch(deg: number): number { + public degToScratch(deg: number): number { return -deg + 90; } - scratchToDeg(scratchDir: number): number { + public scratchToDeg(scratchDir: number): number { return -scratchDir + 90; } - radToScratch(rad: number): number { + public radToScratch(rad: number): number { return this.degToScratch(this.radToDeg(rad)); } - scratchToRad(scratchDir: number): number { + public scratchToRad(scratchDir: number): number { return this.degToRad(this.scratchToDeg(scratchDir)); } @@ -262,7 +264,7 @@ abstract class SpriteBase { } // Wrap rotation from -180 to 180. - normalizeDeg(deg: number): number { + public normalizeDeg(deg: number): number { // This is a pretty big math expression, but it's necessary because in JavaScript, // the % operator means "remainder", not "modulo", and so negative numbers won't "wrap around". // See https://web.archive.org/web/20090717035140if_/javascript.about.com/od/problemsolving/a/modulobug.htm @@ -280,7 +282,7 @@ abstract class SpriteBase { } // Given a generator function, return a version of it that runs in "warp mode" (no yields). - warp(procedure: GeneratorFunction): (...args: unknown[]) => void { + public warp(procedure: GeneratorFunction): (...args: unknown[]) => void { const bound = procedure.bind(this); return (...args) => { const inst = bound(...args); @@ -289,7 +291,7 @@ abstract class SpriteBase { } // TODO: this should also take strings so rand("0.0", "1.0") returns a random float like Scratch - random(a: number, b: number): number { + public random(a: number, b: number): number { const min = Math.min(a, b); const max = Math.max(a, b); if (min % 1 === 0 && max % 1 === 0) { @@ -298,7 +300,7 @@ abstract class SpriteBase { return Math.random() * (max - min) + min; } - *wait(secs: number): Yielding { + public *wait(secs: number): Yielding { const endTime = new Date(); endTime.setMilliseconds(endTime.getMilliseconds() + secs * 1000); while (new Date() < endTime) { @@ -306,23 +308,23 @@ abstract class SpriteBase { } } - get mouse(): Mouse { + public get mouse(): Mouse { return this._project.input.mouse; } - keyPressed(name: string): boolean { + public keyPressed(name: string): boolean { return this._project.input.keyPressed(name); } - get timer(): number { + public get timer(): number { return this._project.timer; } - restartTimer(): void { + public restartTimer(): void { this._project.restartTimer(); } - *startSound(soundName: string): Yielding { + public *startSound(soundName: string): Yielding { const sound = this.getSound(soundName); if (sound) { this.effectChain.applyToSound(sound); @@ -330,7 +332,7 @@ abstract class SpriteBase { } } - *playSoundUntilDone(soundName: string): Yielding { + public *playSoundUntilDone(soundName: string): Yielding { const sound = this.getSound(soundName); if (sound) { sound.connect(this.effectChain.inputNode); @@ -339,7 +341,7 @@ abstract class SpriteBase { } } - getSound(soundName: string): Sound | undefined { + public getSound(soundName: string): Sound | undefined { if (typeof soundName === "number") { return this.sounds[(soundName - 1) % this.sounds.length]; } else { @@ -347,21 +349,21 @@ abstract class SpriteBase { } } - stopAllSounds(): void { + public stopAllSounds(): void { this._project.stopAllSounds(); } - stopAllOfMySounds(): void { + public stopAllOfMySounds(): void { for (const sound of this.sounds) { sound.stop(); } } - broadcast(name: string): Promise { + public broadcast(name: string): Promise { return this._project.fireTrigger(Trigger.BROADCAST, { name }); } - *broadcastAndWait(name: string): Yielding { + public *broadcastAndWait(name: string): Yielding { let running = true; void this.broadcast(name).then(() => { running = false; @@ -372,11 +374,11 @@ abstract class SpriteBase { } } - clearPen(): void { + public clearPen(): void { this._project.renderer.clearPen(); } - *askAndWait(question: string): Yielding { + public *askAndWait(question: string): Yielding { if (this._speechBubble && this instanceof Sprite) { this.say(""); } @@ -389,11 +391,11 @@ abstract class SpriteBase { while (!done) yield; } - get answer(): string | null { + public get answer(): string | null { return this._project.answer; } - get loudness(): number { + public get loudness(): number { return this._project.loudness; } @@ -511,21 +513,21 @@ type SpriteInitialConditions = { }; export class Sprite extends SpriteBase { - _x: number; - _y: number; - _direction: number; - rotationStyle: RotationStyle; - size: number; - visible: boolean; + private _x: number; + private _y: number; + private _direction: number; + public rotationStyle: RotationStyle; + public size: number; + public visible: boolean; - parent: Sprite | null; - clones: Sprite[]; + private parent: Sprite | null; + public clones: Sprite[]; - _penDown: boolean; - penSize: number; - _penColor: Color; + private _penDown: boolean; + public penSize: number; + private _penColor: Color; - constructor(initialConditions: SpriteInitialConditions, vars: Vars) { + public constructor(initialConditions: SpriteInitialConditions, vars: Vars) { super(initialConditions, vars); const { @@ -563,17 +565,14 @@ export class Sprite extends SpriteBase { }; } - createClone(): void { + public createClone(): void { const clone = Object.assign( Object.create(Object.getPrototypeOf(this) as object) as Sprite, this ); clone._project = this._project; - clone.triggers = this.triggers.map( - (trigger) => - new Trigger(trigger.trigger, trigger.options, trigger._script) - ); + clone.triggers = this.triggers.map((trigger) => trigger.clone()); clone.costumes = this.costumes; clone.sounds = this.sounds; clone._vars = Object.assign({}, this._vars); @@ -613,7 +612,7 @@ export class Sprite extends SpriteBase { ); } - deleteThisClone(): void { + public deleteThisClone(): void { if (this.parent === null) return; this.parent.clones = this.parent.clones.filter((clone) => clone !== this); @@ -624,19 +623,19 @@ export class Sprite extends SpriteBase { } // TODO: is this necessary now that the clone hierarchy seems to be flattened? - andClones(): Sprite[] { + public andClones(): Sprite[] { return [this, ...this.clones.flatMap((clone) => clone.andClones())]; } - get direction(): number { + public get direction(): number { return this._direction; } - set direction(dir) { + public set direction(dir) { this._direction = this.normalizeDeg(dir); } - goto(x: number, y: number): void { + public goto(x: number, y: number): void { if (x === this.x && y === this.y) return; if (this.penDown) { @@ -652,23 +651,23 @@ export class Sprite extends SpriteBase { this._y = y; } - get x(): number { + public get x(): number { return this._x; } - set x(x) { + public set x(x) { this.goto(x, this._y); } - get y(): number { + public get y(): number { return this._y; } - set y(y) { + public set y(y) { this.goto(this._x, y); } - move(dist: number): void { + public move(dist: number): void { const moveDir = this.scratchToRad(this.direction); this.goto( @@ -677,7 +676,7 @@ export class Sprite extends SpriteBase { ); } - *glide(seconds: number, x: number, y: number): Yielding { + public *glide(seconds: number, x: number, y: number): Yielding { const interpolate = (a: number, b: number, t: number): number => a + (b - a) * t; @@ -693,7 +692,7 @@ export class Sprite extends SpriteBase { } while (t < 1); } - moveAhead(value = Infinity): void { + public moveAhead(value = Infinity): void { if (typeof value === "number") { this._project.changeSpriteLayer(this, value); } else { @@ -701,7 +700,7 @@ export class Sprite extends SpriteBase { } } - moveBehind(value = Infinity): void { + public moveBehind(value = Infinity): void { if (typeof value === "number") { this._project.changeSpriteLayer(this, -value); } else { @@ -709,11 +708,11 @@ export class Sprite extends SpriteBase { } } - get penDown(): boolean { + public get penDown(): boolean { return this._penDown; } - set penDown(penDown) { + public set penDown(penDown) { if (penDown) { this._project.renderer.penLine( { x: this.x, y: this.y }, @@ -725,11 +724,11 @@ export class Sprite extends SpriteBase { this._penDown = penDown; } - get penColor(): Color { + public get penColor(): Color { return this._penColor; } - set penColor(color: unknown) { + public set penColor(color: unknown) { if (color instanceof Color) { this._penColor = color; } else { @@ -739,11 +738,14 @@ export class Sprite extends SpriteBase { } } - stamp(): void { + public stamp(): void { this._project.renderer.stamp(this); } - touching(target: "mouse" | "edge" | Sprite | Stage, fast = false): boolean { + public touching( + target: "mouse" | "edge" | Sprite | Stage, + fast = false + ): boolean { if (typeof target === "string") { switch (target) { case "mouse": @@ -780,7 +782,7 @@ export class Sprite extends SpriteBase { return this._project.renderer.checkSpriteCollision(this, target, fast); } - colorTouching(color: Color, target: Sprite | Stage): boolean { + public colorTouching(color: Color, target: Sprite | Stage): boolean { if (typeof target === "string") { console.error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -811,17 +813,17 @@ export class Sprite extends SpriteBase { } } - say(text: string): void { + public say(text: string): void { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); this._speechBubble = { text: String(text), style: "say", timeout: null }; } - think(text: string): void { + public think(text: string): void { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); this._speechBubble = { text: String(text), style: "think", timeout: null }; } - *sayAndWait(text: string, seconds: number): Yielding { + public *sayAndWait(text: string, seconds: number): Yielding { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); const speechBubble: SpeechBubble = { text, style: "say", timeout: null }; @@ -837,7 +839,7 @@ export class Sprite extends SpriteBase { while (!done) yield; } - *thinkAndWait(text: string, seconds: number): Yielding { + public *thinkAndWait(text: string, seconds: number): Yielding { if (this._speechBubble?.timeout) clearTimeout(this._speechBubble.timeout); const speechBubble: SpeechBubble = { text, style: "think", timeout: null }; @@ -853,7 +855,7 @@ export class Sprite extends SpriteBase { while (!done) yield; } - static RotationStyle = Object.freeze({ + public static RotationStyle = Object.freeze({ ALL_AROUND: Symbol("ALL_AROUND"), LEFT_RIGHT: Symbol("LEFT_RIGHT"), DONT_ROTATE: Symbol("DONT_ROTATE"), @@ -866,11 +868,11 @@ type StageInitialConditions = { } & InitialConditions; export class Stage extends SpriteBase { - readonly width!: number; - readonly height!: number; - __counter: number; + public readonly width!: number; + public readonly height!: number; + public __counter: number; - constructor(initialConditions: StageInitialConditions, vars: Vars) { + public constructor(initialConditions: StageInitialConditions, vars: Vars) { super(initialConditions, vars); // Use defineProperties to make these non-writable. @@ -890,7 +892,7 @@ export class Stage extends SpriteBase { this.__counter = 0; } - fireBackdropChanged(): Promise { + public fireBackdropChanged(): Promise { return this._project.fireTrigger(Trigger.BACKDROP_CHANGED, { backdrop: this.costume.name, }); diff --git a/src/Trigger.ts b/src/Trigger.ts index f4f525f..05a578e 100644 --- a/src/Trigger.ts +++ b/src/Trigger.ts @@ -18,14 +18,14 @@ type TriggerOption = type TriggerOptions = Partial>; export default class Trigger { - trigger: symbol; - options: TriggerOptions; - _script: GeneratorFunction; - _runningScript: Generator | undefined; - done: boolean; - stop: () => void; - - constructor( + public trigger: symbol; + private options: TriggerOptions; + private _script: GeneratorFunction; + private _runningScript: Generator | undefined; + public done: boolean; + private stop: () => void; + + public constructor( trigger: Trigger["trigger"], options: Trigger["options"] | Trigger["_script"], script?: Trigger["_script"] @@ -45,7 +45,7 @@ export default class Trigger { this.stop = () => {}; } - get isEdgeActivated(): boolean { + public get isEdgeActivated(): boolean { return ( this.trigger === TIMER_GREATER_THAN || this.trigger === LOUDNESS_GREATER_THAN @@ -54,7 +54,7 @@ export default class Trigger { // Evaluate the given trigger option, whether it's a value or a function that // returns a value given a target - option( + public option( option: string, target: Sprite | Stage ): number | string | boolean | undefined { @@ -67,7 +67,7 @@ export default class Trigger { return triggerOption; } - matches( + public matches( trigger: Trigger["trigger"], options: Trigger["options"] | undefined, target: Sprite | Stage @@ -80,7 +80,7 @@ export default class Trigger { return true; } - start(target: Sprite | Stage): Promise { + public start(target: Sprite | Stage): Promise { this.stop(); const boundScript = this._script.bind(target); @@ -96,20 +96,24 @@ export default class Trigger { }); } - step(): void { + public step(): void { if (!this._runningScript) return; this.done = !!this._runningScript.next().done; if (this.done) this.stop(); } - static GREEN_FLAG = GREEN_FLAG; - static KEY_PRESSED = KEY_PRESSED; - static BROADCAST = BROADCAST; - static CLICKED = CLICKED; - static CLONE_START = CLONE_START; - static LOUDNESS_GREATER_THAN = LOUDNESS_GREATER_THAN; - static TIMER_GREATER_THAN = TIMER_GREATER_THAN; - static BACKDROP_CHANGED = BACKDROP_CHANGED; + public clone(): Trigger { + return new Trigger(this.trigger, this.options, this._script); + } + + public static GREEN_FLAG = GREEN_FLAG; + public static KEY_PRESSED = KEY_PRESSED; + public static BROADCAST = BROADCAST; + public static CLICKED = CLICKED; + public static CLONE_START = CLONE_START; + public static LOUDNESS_GREATER_THAN = LOUDNESS_GREATER_THAN; + public static TIMER_GREATER_THAN = TIMER_GREATER_THAN; + public static BACKDROP_CHANGED = BACKDROP_CHANGED; } export type { TriggerOption, TriggerOptions }; diff --git a/src/Watcher.ts b/src/Watcher.ts index 05cf0b4..2c7ff61 100644 --- a/src/Watcher.ts +++ b/src/Watcher.ts @@ -25,29 +25,29 @@ type WatcherOptions = { }; export default class Watcher { - value: () => WatcherValue; - setValue: (value: number) => void; - _previousValue: unknown | symbol; - color: Color; - _label!: string; - _x!: number; - _y!: number; - _width: number | undefined; - _height: number | undefined; - _min!: number; - _max!: number; - _step!: number; - _style!: WatcherStyle; - _visible!: boolean; - - _dom!: { + public value: () => WatcherValue; + public setValue: (value: number) => void; + private _previousValue: unknown | symbol; + private color: Color; + private _label!: string; + private _x!: number; + private _y!: number; + private _width: number | undefined; + private _height: number | undefined; + private _min!: number; + private _max!: number; + private _step!: number; + private _style!: WatcherStyle; + private _visible!: boolean; + + private _dom!: { node: HTMLElement; label: HTMLElement; value: HTMLElement; slider: HTMLInputElement; }; - constructor({ + public constructor({ value = () => "", // eslint-disable-next-line @typescript-eslint/no-empty-function setValue = () => {}, @@ -86,7 +86,7 @@ export default class Watcher { this.step = 1; } - initializeDOM(): void { + private initializeDOM(): void { const node = document.createElement("div"); node.classList.add("leopard__watcher"); @@ -111,7 +111,7 @@ export default class Watcher { this._dom = { node, label, value, slider }; } - updateDOM(renderTarget: HTMLElement | null): void { + public updateDOM(renderTarget: HTMLElement | null): void { if (renderTarget && !renderTarget.contains(this._dom.node)) { renderTarget.append(this._dom.node); } @@ -177,34 +177,34 @@ export default class Watcher { this._dom.value.style.setProperty("--watcher-text-color", textColor); } - get visible(): boolean { + public get visible(): boolean { return this._visible; } - set visible(visible) { + public set visible(visible) { this._visible = visible; this._dom.node.style.visibility = visible ? "visible" : "hidden"; } - get x(): number { + public get x(): number { return this._x; } - set x(x) { + public set x(x) { this._x = x; this._dom.node.style.left = `${x - 240}px`; } - get y(): number { + public get y(): number { return this._y; } - set y(y) { + public set y(y) { this._y = y; this._dom.node.style.top = `${180 - y}px`; } - get width(): number | undefined { + public get width(): number | undefined { return this._width; } - set width(width) { + public set width(width) { this._width = width; if (width) { this._dom.node.style.width = `${width}px`; @@ -213,10 +213,10 @@ export default class Watcher { } } - get height(): number | undefined { + public get height(): number | undefined { return this._height; } - set height(height) { + public set height(height) { this._height = height; if (height) { this._dom.node.style.height = `${height}px`; @@ -225,10 +225,10 @@ export default class Watcher { } } - get style(): WatcherStyle { + public get style(): WatcherStyle { return this._style; } - set style(style) { + public set style(style) { this._style = style; this._dom.node.classList.toggle( "leopard__watcher--normal", @@ -244,34 +244,34 @@ export default class Watcher { ); } - get min(): number { + public get min(): number { return this._min; } - set min(min: number) { + public set min(min: number) { this._min = min; this._dom.slider.min = String(min); } - get max(): number { + public get max(): number { return this._max; } - set max(max: number) { + public set max(max: number) { this._max = max; this._dom.slider.max = String(max); } - get step(): number { + public get step(): number { return this._step; } - set step(step) { + public set step(step) { this._step = step; this._dom.slider.step = String(step); } - get label(): string { + public get label(): string { return this._label; } - set label(label) { + public set label(label) { this._label = label; this._dom.label.innerText = label; } diff --git a/src/renderer/BitmapSkin.ts b/src/renderer/BitmapSkin.ts index e37363d..de6be8f 100644 --- a/src/renderer/BitmapSkin.ts +++ b/src/renderer/BitmapSkin.ts @@ -2,11 +2,11 @@ import type Renderer from "../Renderer"; import Skin from "./Skin"; export default class BitmapSkin extends Skin { - _image: HTMLImageElement; - _imageData: ImageData | null; - _texture: WebGLTexture | null; + private _image: HTMLImageElement; + private _imageData: ImageData | null; + private _texture: WebGLTexture | null; - constructor(renderer: Renderer, image: HTMLImageElement) { + public constructor(renderer: Renderer, image: HTMLImageElement) { super(renderer); this._image = image; @@ -16,7 +16,7 @@ export default class BitmapSkin extends Skin { this._setSizeFromImage(image); } - getImageData(): ImageData | null { + public getImageData(): ImageData | null { // Make sure to handle potentially non-loaded textures if (!this._image.complete) return null; @@ -34,7 +34,7 @@ export default class BitmapSkin extends Skin { return this._imageData; } - getTexture(): WebGLTexture | null { + public getTexture(): WebGLTexture | null { // Make sure to handle potentially non-loaded textures const image = this._image; if (!image.complete) return null; @@ -46,7 +46,7 @@ export default class BitmapSkin extends Skin { return this._texture; } - destroy(): void { + public destroy(): void { if (this._texture !== null) this.gl.deleteTexture(this._texture); } } diff --git a/src/renderer/Drawable.ts b/src/renderer/Drawable.ts index c29d8d4..89f7f00 100644 --- a/src/renderer/Drawable.ts +++ b/src/renderer/Drawable.ts @@ -24,24 +24,24 @@ const determinant = ( // TODO: store renderer-specific data on the sprite and have *it* set a // "transform changed" flag. class SpriteTransformDiff { - _sprite: Sprite | Stage; - _unset: boolean; - - _lastX!: Sprite["x"] | undefined; - _lastY!: Sprite["y"] | undefined; - _lastRotation!: Sprite["direction"] | undefined; - _lastRotationStyle!: Sprite["rotationStyle"] | undefined; - _lastSize!: Sprite["size"] | undefined; - _lastCostume!: Sprite["costume"]; - _lastCostumeLoaded!: boolean; - - constructor(sprite: Sprite | Stage) { + private _sprite: Sprite | Stage; + private _unset: boolean; + + private _lastX!: Sprite["x"] | undefined; + private _lastY!: Sprite["y"] | undefined; + private _lastRotation!: Sprite["direction"] | undefined; + private _lastRotationStyle!: Sprite["rotationStyle"] | undefined; + private _lastSize!: Sprite["size"] | undefined; + private _lastCostume!: Sprite["costume"]; + private _lastCostumeLoaded!: boolean; + + public constructor(sprite: Sprite | Stage) { this._sprite = sprite; this._unset = true; this.update(); } - update(): void { + public update(): void { if (this._sprite instanceof Sprite) { this._lastX = this._sprite.x; this._lastY = this._sprite.y; @@ -54,7 +54,7 @@ class SpriteTransformDiff { this._unset = false; } - get changed(): boolean { + public get changed(): boolean { return ( (this._sprite instanceof Sprite && (this._lastX !== this._sprite.x || @@ -71,23 +71,24 @@ class SpriteTransformDiff { // Renderer-specific data for an instance (the original or a clone) of a Sprite export default class Drawable { - _renderer: Renderer; - _sprite: Sprite | Stage; - _matrix: MatrixType; - _matrixDiff: SpriteTransformDiff; - - _convexHullImageData: ImageData | null; - _convexHullMosaic: number; - _convexHullPixelate: number; - _convexHullWhirl: number; - _convexHullFisheye: number; - _convexHullPoints: [number, number][] | null; - - _aabb: Rectangle; - _tightBoundingBox: Rectangle; - _convexHullMatrixDiff: SpriteTransformDiff; - - constructor(renderer: Renderer, sprite: Sprite | Stage) { + private _renderer: Renderer; + // TODO: make this private + public _sprite: Sprite | Stage; + private _matrix: MatrixType; + private _matrixDiff: SpriteTransformDiff; + + private _convexHullImageData: ImageData | null; + private _convexHullMosaic: number; + private _convexHullPixelate: number; + private _convexHullWhirl: number; + private _convexHullFisheye: number; + private _convexHullPoints: [number, number][] | null; + + private _aabb: Rectangle; + private _tightBoundingBox: Rectangle; + private _convexHullMatrixDiff: SpriteTransformDiff; + + public constructor(renderer: Renderer, sprite: Sprite | Stage) { this._renderer = renderer; this._sprite = sprite; @@ -115,18 +116,18 @@ export default class Drawable { this._convexHullMatrixDiff = new SpriteTransformDiff(sprite); } - getCurrentSkin(): Skin { + public getCurrentSkin(): Skin { return this._renderer._getSkin(this._sprite.costume); } // Get the rough axis-aligned bounding box for this sprite. Not as tight as // getTightBoundingBox, especially when rotated. - getAABB(): Rectangle { + public getAABB(): Rectangle { return Rectangle.fromMatrix(this.getMatrix(), this._aabb); } // Get the Scratch-space tight bounding box for this sprite. - getTightBoundingBox(): Rectangle { + public getTightBoundingBox(): Rectangle { if (!this._convexHullMatrixDiff.changed) return this._tightBoundingBox; const matrix = this.getMatrix(); @@ -190,7 +191,7 @@ export default class Drawable { return this._tightBoundingBox; } - _calculateConvexHull(): [number, number][] | null { + private _calculateConvexHull(): [number, number][] | null { const sprite = this._sprite; const skin = this.getCurrentSkin(); const imageData = skin.getImageData( @@ -327,7 +328,7 @@ export default class Drawable { return this._convexHullPoints; } - _calculateSpriteMatrix(): void { + private _calculateSpriteMatrix(): void { const m = this._matrix; Matrix.identity(m); const spr = this._sprite; @@ -369,7 +370,7 @@ export default class Drawable { this._matrixDiff.update(); } - getMatrix(): MatrixType { + public getMatrix(): MatrixType { // If all the values we used to calculate the matrix haven't changed since // we last calculated the matrix, we can just return the matrix as-is. if (this._matrixDiff.changed) { diff --git a/src/renderer/Matrix.ts b/src/renderer/Matrix.ts index e4e8143..309f851 100644 --- a/src/renderer/Matrix.ts +++ b/src/renderer/Matrix.ts @@ -5,14 +5,14 @@ // 3x3 transform matrix operations, unrolled 4 da speedz. export default class Matrix { // Create a new 3x3 transform matrix, initialized to the identity matrix. - static create(): MatrixType { + public static create(): MatrixType { const matrix = new Float32Array(9); Matrix.identity(matrix); return matrix; } // Reset a matrix to the identity matrix - static identity(dst: MatrixType): MatrixType { + public static identity(dst: MatrixType): MatrixType { dst[0] = 1; dst[1] = 0; dst[2] = 0; @@ -26,7 +26,7 @@ export default class Matrix { } // Translate a matrix by the given X and Y values - static translate( + public static translate( dst: MatrixType, src: MatrixType, x: number, @@ -57,7 +57,11 @@ export default class Matrix { } // Rotate a matrix, in radians - static rotate(dst: MatrixType, src: MatrixType, rad: number): MatrixType { + public static rotate( + dst: MatrixType, + src: MatrixType, + rad: number + ): MatrixType { const a00 = src[0], a01 = src[1], a02 = src[2], @@ -85,7 +89,7 @@ export default class Matrix { } // Scale a matrix by the given X and Y values - static scale( + public static scale( dst: MatrixType, src: MatrixType, x: number, @@ -106,7 +110,7 @@ export default class Matrix { } // Transform a 2D point by the given matrix - static transformPoint( + public static transformPoint( m: MatrixType, dst: [number, number], src: [number, number] diff --git a/src/renderer/PenSkin.ts b/src/renderer/PenSkin.ts index b247f7b..bbac7de 100644 --- a/src/renderer/PenSkin.ts +++ b/src/renderer/PenSkin.ts @@ -5,13 +5,13 @@ import type Renderer from "../Renderer"; import type { FramebufferInfo } from "../Renderer"; export default class PenSkin extends Skin { - _framebufferInfo: FramebufferInfo; - _lastPenState: { + public _framebufferInfo: FramebufferInfo; + private _lastPenState: { size: number; color: [number, number, number, number]; }; - constructor(renderer: Renderer, width: number, height: number) { + public constructor(renderer: Renderer, width: number, height: number) { super(renderer); this.width = width; this.height = height; @@ -31,17 +31,17 @@ export default class PenSkin extends Skin { this.clear(); } - destroy(): void { + public destroy(): void { const gl = this.gl; gl.deleteTexture(this._framebufferInfo.texture); gl.deleteFramebuffer(this._framebufferInfo.framebuffer); } - getTexture(): WebGLTexture { + public getTexture(): WebGLTexture { return this._framebufferInfo.texture; } - penLine( + public penLine( pt1: { x: number; y: number }, pt2: { x: number; y: number }, color: Color, @@ -116,7 +116,7 @@ export default class PenSkin extends Skin { gl.drawArrays(gl.TRIANGLES, 0, 6); } - clear(): void { + public clear(): void { this.renderer._setFramebuffer(this._framebufferInfo); const gl = this.gl; gl.clearColor(0, 0, 0, 0); diff --git a/src/renderer/Rectangle.ts b/src/renderer/Rectangle.ts index c1f4b2f..66a668e 100644 --- a/src/renderer/Rectangle.ts +++ b/src/renderer/Rectangle.ts @@ -1,12 +1,12 @@ import type { MatrixType } from "./Matrix"; export default class Rectangle { - left: number; - right: number; - bottom: number; - top: number; + public left: number; + public right: number; + public bottom: number; + public top: number; - constructor() { + public constructor() { this.left = -Infinity; this.right = Infinity; this.bottom = -Infinity; @@ -15,7 +15,7 @@ export default class Rectangle { return this; } - static fromBounds( + public static fromBounds( left: number, right: number, bottom: number, @@ -31,7 +31,10 @@ export default class Rectangle { } // Initialize a bounding box around a sprite given the sprite's transform matrix. - static fromMatrix(matrix: MatrixType, result = new Rectangle()): Rectangle { + public static fromMatrix( + matrix: MatrixType, + result = new Rectangle() + ): Rectangle { // Adapted somewhat from https://github.com/LLK/scratch-render/blob/develop/docs/Rectangle-AABB-Matrix.md const xa = matrix[0] / 2; const xb = matrix[3] / 2; @@ -52,7 +55,7 @@ export default class Rectangle { } // Initialize from another rectangle. - static copy(src: Rectangle, dst: Rectangle): Rectangle { + public static copy(src: Rectangle, dst: Rectangle): Rectangle { dst.left = src.left; dst.right = src.right; dst.bottom = src.bottom; @@ -62,7 +65,7 @@ export default class Rectangle { // Push this rectangle out to integer bounds. // This takes a conservative approach and will always expand the rectangle outwards. - snapToInt(): this { + public snapToInt(): this { this.left = Math.floor(this.left); this.right = Math.ceil(this.right); this.bottom = Math.floor(this.bottom); @@ -72,7 +75,7 @@ export default class Rectangle { } // Check whether any part of this rectangle touches another rectangle. - intersects(rect: Rectangle): boolean { + public intersects(rect: Rectangle): boolean { return ( this.left <= rect.right && rect.left <= this.right && @@ -82,14 +85,14 @@ export default class Rectangle { } // Check whether a given point is inside this rectangle. - containsPoint(x: number, y: number): boolean { + public containsPoint(x: number, y: number): boolean { return ( x >= this.left && x <= this.right && y >= this.bottom && y <= this.top ); } // Clamp this rectangle within bounds. - clamp(left: number, right: number, bottom: number, top: number): this { + public clamp(left: number, right: number, bottom: number, top: number): this { this.left = Math.min(Math.max(this.left, left), right); this.right = Math.max(Math.min(this.right, right), left); this.bottom = Math.min(Math.max(this.bottom, bottom), top); @@ -99,7 +102,7 @@ export default class Rectangle { } // Compute the union of two rectangles. - static union( + public static union( rect1: Rectangle, rect2: Rectangle, result = new Rectangle() @@ -113,7 +116,7 @@ export default class Rectangle { } // Compute the intersection of two rectangles. - static intersection( + public static intersection( rect1: Rectangle, rect2: Rectangle, result = new Rectangle() @@ -126,11 +129,11 @@ export default class Rectangle { return result; } - get width(): number { + public get width(): number { return this.right - this.left; } - get height(): number { + public get height(): number { return this.top - this.bottom; } } diff --git a/src/renderer/ShaderManager.ts b/src/renderer/ShaderManager.ts index 6a20681..8ed6849 100644 --- a/src/renderer/ShaderManager.ts +++ b/src/renderer/ShaderManager.ts @@ -4,13 +4,13 @@ import type Renderer from "../Renderer"; // Everything contained in a shader. It contains both the program, and the locations of the shader inputs. class Shader { - gl: WebGLRenderingContext; - program: WebGLProgram; + private gl: WebGLRenderingContext; + public program: WebGLProgram; // TODO: strongly type these - uniforms: Record; - attribs: Record; + public uniforms: Record; + public attribs: Record; - constructor(gl: WebGLRenderingContext, program: WebGLProgram) { + public constructor(gl: WebGLRenderingContext, program: WebGLProgram) { this.gl = gl; this.program = program; this.uniforms = {}; @@ -42,12 +42,12 @@ class Shader { type DrawMode = keyof typeof ShaderManager["DrawModes"]; class ShaderManager { - renderer: Renderer; - gl: WebGLRenderingContext; + private renderer: Renderer; + private gl: WebGLRenderingContext; - _shaderCache: Record>; + private _shaderCache: Record>; - constructor(renderer: Renderer) { + public constructor(renderer: Renderer) { this.renderer = renderer; this.gl = renderer.gl; @@ -59,7 +59,7 @@ class ShaderManager { } // Creates and compiles a vertex or fragment shader from the given source code. - _createShader( + private _createShader( source: string, type: | WebGLRenderingContext["FRAGMENT_SHADER"] @@ -79,7 +79,7 @@ class ShaderManager { return shader; } - getShader(drawMode: DrawMode, effectBitmask = 0): Shader { + public getShader(drawMode: DrawMode, effectBitmask = 0): Shader { const gl = this.gl; // Each combination of enabled effects is compiled to a different shader, with only the needed effect code. // Check if we've already compiled the shader with this set of enabled effects. @@ -136,7 +136,7 @@ class ShaderManager { return shader; } - static DrawModes = { + public static DrawModes = { // Used for drawing sprites normally DEFAULT: "DEFAULT", // Used for "touching" tests. Discards transparent pixels. diff --git a/src/renderer/Skin.ts b/src/renderer/Skin.ts index 53164bb..d8ed333 100644 --- a/src/renderer/Skin.ts +++ b/src/renderer/Skin.ts @@ -1,12 +1,12 @@ import type Renderer from "../Renderer"; export default abstract class Skin { - renderer: Renderer; - gl: WebGLRenderingContext; - width: number; - height: number; + protected renderer: Renderer; + protected gl: WebGLRenderingContext; + public width: number; + public height: number; - constructor(renderer: Renderer) { + public constructor(renderer: Renderer) { this.renderer = renderer; this.gl = renderer.gl; this.width = 0; @@ -15,16 +15,16 @@ export default abstract class Skin { // Get the skin's texture for a given (screen-space) scale. // eslint-disable-next-line @typescript-eslint/no-unused-vars - abstract getTexture(scale: number): WebGLTexture | null; + public abstract getTexture(scale: number): WebGLTexture | null; // Get the skin image's ImageData at a given (screen-space) scale. // eslint-disable-next-line @typescript-eslint/no-unused-vars - getImageData(scale: number): ImageData | null { + public getImageData(scale: number): ImageData | null { throw new Error("getImageData not implemented for this skin type"); } // Helper function to create a texture from an image and handle all the boilerplate. - _makeTexture( + protected _makeTexture( image: HTMLImageElement | HTMLCanvasElement | null, filtering: | WebGLRenderingContext["NEAREST"] @@ -55,7 +55,7 @@ export default abstract class Skin { } // Helper function to set this skin's size based on an image that may or may not be loaded. - _setSizeFromImage(image: HTMLImageElement): void { + protected _setSizeFromImage(image: HTMLImageElement): void { if (image.complete) { this.width = image.naturalWidth; this.height = image.naturalHeight; @@ -68,5 +68,5 @@ export default abstract class Skin { } // Clean up any textures or other objets created by this skin. - abstract destroy(): void; + public abstract destroy(): void; } diff --git a/src/renderer/SpeechBubbleSkin.ts b/src/renderer/SpeechBubbleSkin.ts index 5e886a7..917885e 100644 --- a/src/renderer/SpeechBubbleSkin.ts +++ b/src/renderer/SpeechBubbleSkin.ts @@ -12,17 +12,17 @@ const bubbleStyle = { // TODO: multiline speech bubbles export default class SpeechBubbleSkin extends Skin { - _canvas: HTMLCanvasElement; - _ctx: CanvasRenderingContext2D; - _texture: WebGLTexture; - _bubble: SpeechBubble; - _flipped: boolean; - _rendered: boolean; - _renderedScale: number; - offsetX: number; - offsetY: number; - - constructor(renderer: Renderer, bubble: SpeechBubble) { + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + private _texture: WebGLTexture; + private _bubble: SpeechBubble; + private _flipped: boolean; + private _rendered: boolean; + private _renderedScale: number; + public offsetX: number; + public offsetY: number; + + public constructor(renderer: Renderer, bubble: SpeechBubble) { super(renderer); this._canvas = document.createElement("canvas"); @@ -42,18 +42,18 @@ export default class SpeechBubbleSkin extends Skin { } // To ensure proper text measurement and drawing, it's necessary to restyle the canvas after resizing it. - _restyleCanvas(): void { + private _restyleCanvas(): void { const ctx = this._ctx; ctx.font = "16px sans-serif"; ctx.textBaseline = "hanging"; } - set flipped(flipped: boolean) { + public set flipped(flipped: boolean) { this._flipped = flipped; this._rendered = false; } - _renderBubble(bubble: SpeechBubble, scale: number): void { + private _renderBubble(bubble: SpeechBubble, scale: number): void { const canvas = this._canvas; const ctx = this._ctx; @@ -144,7 +144,7 @@ export default class SpeechBubbleSkin extends Skin { this._renderedScale = scale; } - getTexture(scale: number): WebGLTexture { + public getTexture(scale: number): WebGLTexture { if (!this._rendered || this._renderedScale !== scale) { this._renderBubble(this._bubble, scale); const gl = this.gl; @@ -162,7 +162,7 @@ export default class SpeechBubbleSkin extends Skin { return this._texture; } - destroy(): void { + public destroy(): void { this.gl.deleteTexture(this._texture); } } diff --git a/src/renderer/VectorSkin.ts b/src/renderer/VectorSkin.ts index 55bfd86..065b403 100644 --- a/src/renderer/VectorSkin.ts +++ b/src/renderer/VectorSkin.ts @@ -5,15 +5,15 @@ import type Renderer from "../Renderer"; const MIPMAP_OFFSET = 4; export default class VectorSkin extends Skin { - _image: HTMLImageElement; - _canvas: HTMLCanvasElement; - _ctx: CanvasRenderingContext2D; - _imageDataMipLevel: number; - _imageData: ImageData | null; - _maxTextureSize: number; - _mipmaps: Map; - - constructor(renderer: Renderer, image: HTMLImageElement) { + private _image: HTMLImageElement; + private _canvas: HTMLCanvasElement; + private _ctx: CanvasRenderingContext2D; + private _imageDataMipLevel: number; + private _imageData: ImageData | null; + private _maxTextureSize: number; + private _mipmaps: Map; + + public constructor(renderer: Renderer, image: HTMLImageElement) { super(renderer); this._image = image; @@ -34,11 +34,11 @@ export default class VectorSkin extends Skin { this._mipmaps = new Map(); } - static mipLevelForScale(scale: number): number { + private static mipLevelForScale(scale: number): number { return Math.max(Math.ceil(Math.log2(scale)) + MIPMAP_OFFSET, 0); } - getImageData(scale: number): ImageData | null { + public getImageData(scale: number): ImageData | null { if (!this._image.complete) return null; // Round off the scale of the image data drawn to a given power-of-two mip level. @@ -56,7 +56,7 @@ export default class VectorSkin extends Skin { return this._imageData; } - _drawSvgToCanvas(mipLevel: number): CanvasRenderingContext2D | null { + private _drawSvgToCanvas(mipLevel: number): CanvasRenderingContext2D | null { const scale = 2 ** (mipLevel - MIPMAP_OFFSET); const image = this._image; @@ -84,7 +84,7 @@ export default class VectorSkin extends Skin { // TODO: handle proper subpixel positioning when SVG viewbox has non-integer coordinates // This will require rethinking costume + project loading probably - _createMipmap(mipLevel: number): void { + private _createMipmap(mipLevel: number): void { // Instead of uploading the image to WebGL as a texture, render the image to a canvas and upload the canvas. const ctx = this._drawSvgToCanvas(mipLevel); this._mipmaps.set( @@ -95,7 +95,7 @@ export default class VectorSkin extends Skin { ); } - getTexture(scale: number): WebGLTexture | null { + public getTexture(scale: number): WebGLTexture | null { if (!this._image.complete) return null; // Because WebGL doesn't support vector graphics, substitute a bunch of bitmaps. @@ -111,7 +111,7 @@ export default class VectorSkin extends Skin { return this._mipmaps.get(mipLevel) ?? null; } - destroy(): void { + public destroy(): void { for (const mip of this._mipmaps.values()) { this.gl.deleteTexture(mip); } From 831adc839f3afead10e37c8b2ee0178a19d586ca Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 06/46] Add effect properties to AudioEffectMap --- src/Sound.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Sound.ts b/src/Sound.ts index 8dcab93..91a8c7f 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -736,7 +736,11 @@ type EffectDescriptor< ? PatchDescriptor : PatchlessDescriptor; -export class AudioEffectMap { +type Effects = { + [x in EffectName]: number; +}; + +export class AudioEffectMap implements Effects { // This class provides a simple interface for setting and getting audio // effects stored on an EffectChain, similar to EffectMap (that class being // for graphic effects). It takes an EffectChain and automatically generates @@ -744,6 +748,11 @@ export class AudioEffectMap { // the EffectChain's API when accessed. private effectChain: EffectChain; + // TypeScript can't infer these + public pan!: number; + public pitch!: number; + public volume!: number; + public constructor(effectChain: EffectChain) { this.effectChain = effectChain; From a52fa2095ffbbfb67278ec24ac9cbb497419d4f4 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 07/46] Remove ESLint comment from abstract method --- src/renderer/Skin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/renderer/Skin.ts b/src/renderer/Skin.ts index d8ed333..3b7eb87 100644 --- a/src/renderer/Skin.ts +++ b/src/renderer/Skin.ts @@ -14,7 +14,6 @@ export default abstract class Skin { } // Get the skin's texture for a given (screen-space) scale. - // eslint-disable-next-line @typescript-eslint/no-unused-vars public abstract getTexture(scale: number): WebGLTexture | null; // Get the skin image's ImageData at a given (screen-space) scale. From 72ec69ccc2f3a50d200e08a2e395058765012972 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 08/46] Port toBoolean + compare to TypeScript --- src/Sprite.ts | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index d2a19af..d4a5993 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -249,7 +249,7 @@ abstract class SpriteBase { } // From scratch-vm's math-util. - scratchTan(angle) { + public scratchTan(angle: number): number { angle = angle % 360; switch (angle) { case -270: @@ -276,9 +276,9 @@ abstract class SpriteBase { // wrapClamp(0, 1, 5) == 5 // wrapClamp(-11, -10, 6) == 6 // Borrowed from scratch-vm (src/util/math-util.js) - wrapClamp(n, min, max) { - const range = (max - min) + 1; - return n - (Math.floor((n - min) / range) * range); + public wrapClamp(n: number, min: number, max: number): number { + const range = max - min + 1; + return n - Math.floor((n - min) / range) * range; } // Given a generator function, return a version of it that runs in "warp mode" (no yields). @@ -399,8 +399,8 @@ abstract class SpriteBase { return this._project.loudness; } - toNumber(value) { - if (typeof value === 'number') { + public toNumber(value: unknown): number { + if (typeof value === "number") { if (isNaN(value)) { return 0; } @@ -414,13 +414,13 @@ abstract class SpriteBase { return n; } - toBoolean(value) { - if (typeof value === 'boolean') { + public toBoolean(value: unknown): boolean { + if (typeof value === "boolean") { return value; } - if (typeof value === 'string') { - if (value === '' || value === '0' || value.toLowerCase() === 'false') { + if (typeof value === "string") { + if (value === "" || value === "0" || value.toLowerCase() === "false") { return false; } return true; @@ -429,37 +429,37 @@ abstract class SpriteBase { return Boolean(value); } - toString(value) { + public toString(value: unknown): string { return String(value); } - stringIncludes(string, substring) { + public stringIncludes(string: string, substring: string): boolean { return string.toLowerCase().includes(substring.toLowerCase()); } - arrayIncludes(array, value) { - return array.some(item => this.compare(item, value) === 0); + public arrayIncludes(array: T[], value: T): boolean { + return array.some((item) => this.compare(item, value) === 0); } - letterOf(string, index) { + public letterOf(string: string, index: number): string { if (index < 0 || index >= string.length) { return ""; } return string[index]; } - itemOf(array, index) { + public itemOf(array: T[], index: number): T | "" { if (index < 0 || index >= array.length) { return ""; } return array[index]; } - indexInArray(array, value) { - return array.findIndex(item => this.compare(item, value) === 0); + public indexInArray(array: T[], value: T): number { + return array.findIndex((item) => this.compare(item, value) === 0); } - compare(v1, v2) { + public compare(v1: unknown, v2: unknown): number { if (v1 === v2) { return 0; } @@ -473,9 +473,15 @@ abstract class SpriteBase { return 0; } - if (n1 === 0 && (v1 === null || typeof v1 === 'string' && v1.trim().length === 0)) { + if ( + n1 === 0 && + (v1 === null || (typeof v1 === "string" && v1.trim().length === 0)) + ) { n1 = NaN; - } else if (n2 === 0 && (v2 === null || typeof v2 === 'string' && v2.trim().length === 0)) { + } else if ( + n2 === 0 && + (v2 === null || (typeof v2 === "string" && v2.trim().length === 0)) + ) { n2 = NaN; } From 81c8dee6fd48f7a449b860381b1f20618fc056fe Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 09/46] Fix edge-activated trigger handling --- src/Project.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Project.ts b/src/Project.ts index 44a4505..70adbd4 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -20,10 +20,16 @@ export default class Project { private _cachedLoudness: number | null; public runningTriggers: TriggerWithTarget[]; - private _prevStepTriggerPredicates: WeakMap; + public answer: string | null; private timerStart!: Date; + /** + * Used to keep track of what edge-activated trigger predicates evaluted to + * on the previous step. + */ + private _prevStepTriggerPredicates: WeakMap; + public constructor(stage: Stage, sprites = {}, { frameRate = 30 } = {}) { this.stage = stage; this.sprites = sprites; @@ -45,8 +51,6 @@ export default class Project { this._cachedLoudness = null; this.runningTriggers = []; - // Used to keep track of what edge-activated trigger predicates evaluted to - // on the previous step. this._prevStepTriggerPredicates = new WeakMap(); this.restartTimer(); @@ -81,7 +85,7 @@ export default class Project { clickedSprite = this.stage; } - const matchingTriggers = []; + const matchingTriggers: TriggerWithTarget[] = []; for (const trigger of clickedSprite.triggers) { if (trigger.matches(Trigger.CLICKED, {}, clickedSprite)) { matchingTriggers.push({ trigger, target: clickedSprite }); @@ -108,7 +112,7 @@ export default class Project { private _matchingTriggers( triggerMatches: (tr: Trigger, target: Sprite | Stage) => boolean ): TriggerWithTarget[] { - const matchingTriggers = []; + const matchingTriggers: TriggerWithTarget[] = []; const targets = this.spritesAndStage; for (const target of targets) { const matchingTargetTriggers = target.triggers.filter((tr) => @@ -123,7 +127,7 @@ export default class Project { private _stepEdgeActivatedTriggers(): void { const edgeActivated = this._matchingTriggers((tr) => tr.isEdgeActivated); - const triggersToStart = []; + const triggersToStart: TriggerWithTarget[] = []; for (const triggerWithTarget of edgeActivated) { const { trigger, target } = triggerWithTarget; let predicate; @@ -139,9 +143,8 @@ export default class Project { } // Default to false - const prevPredicate = - !!this._prevStepTriggerPredicates.get(triggerWithTarget); - this._prevStepTriggerPredicates.set(triggerWithTarget, predicate); + const prevPredicate = !!this._prevStepTriggerPredicates.get(trigger); + this._prevStepTriggerPredicates.set(trigger, predicate); // The predicate evaluated to false last time and true this time // Activate the trigger From b8b8b5e0bcfb159df6f649d71461570ee416eedb Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 10/46] Clean up _startTriggers --- src/Project.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Project.ts b/src/Project.ts index 70adbd4..29dcee7 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -216,7 +216,7 @@ export default class Project { } // TODO: add a way to start clone triggers from fireTrigger then make this private - public _startTriggers(triggers: TriggerWithTarget[]): Promise { + public async _startTriggers(triggers: TriggerWithTarget[]): Promise { // Only add these triggers to this.runningTriggers if they're not already there. // TODO: if the triggers are already running, they'll be restarted but their execution order is unchanged. // Does that match Scratch's behavior? @@ -231,11 +231,9 @@ export default class Project { this.runningTriggers.push(trigger); } } - return Promise.all( - triggers.map(({ trigger, target }) => { - return trigger.start(target); - }) - ).then(); + await Promise.all( + triggers.map(({ trigger, target }) => trigger.start(target)) + ); } public get spritesAndClones(): Sprite[] { From 96a3558779caa1d70a10b8410ff5ed6d2d2da6d2 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 11/46] Change spritesAndStage signature --- src/Project.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Project.ts b/src/Project.ts index 29dcee7..0d1e3e7 100644 --- a/src/Project.ts +++ b/src/Project.ts @@ -242,7 +242,7 @@ export default class Project { .sort((a, b) => a._layerOrder - b._layerOrder); } - public get spritesAndStage(): [...Sprite[], Stage] { + public get spritesAndStage(): (Sprite | Stage)[] { return [...this.spritesAndClones, this.stage]; } From f47427c5c95a75284b7f902b0235ad227b9ccedc Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 12/46] Whitespace tweak in Input --- src/Input.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Input.ts b/src/Input.ts index 4bb53fa..8902f49 100644 --- a/src/Input.ts +++ b/src/Input.ts @@ -9,6 +9,7 @@ export default class Input { public mouse: Mouse; public keys: string[]; + public constructor( stage: Input["_stage"], canvas: Input["_canvas"], From 45933ebf4ca890f4be713310d512d3b9a7ad2be0 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 13/46] Unify constructor param types --- src/Input.ts | 12 ++++++------ src/Trigger.ts | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Input.ts b/src/Input.ts index 8902f49..12ac997 100644 --- a/src/Input.ts +++ b/src/Input.ts @@ -3,17 +3,17 @@ import type { Stage } from "./Sprite"; type Mouse = { x: number; y: number; down: boolean }; export default class Input { - private _stage: Stage; - private _canvas: HTMLCanvasElement; - private _onKeyDown: (key: string) => unknown; + private _stage; + private _canvas; + private _onKeyDown; public mouse: Mouse; public keys: string[]; public constructor( - stage: Input["_stage"], - canvas: Input["_canvas"], - onKeyDown: Input["_onKeyDown"] + stage: Stage, + canvas: HTMLCanvasElement, + onKeyDown: (key: string) => unknown ) { this._stage = stage; this._canvas = canvas; diff --git a/src/Trigger.ts b/src/Trigger.ts index 05a578e..40718ec 100644 --- a/src/Trigger.ts +++ b/src/Trigger.ts @@ -18,7 +18,7 @@ type TriggerOption = type TriggerOptions = Partial>; export default class Trigger { - public trigger: symbol; + public trigger; private options: TriggerOptions; private _script: GeneratorFunction; private _runningScript: Generator | undefined; @@ -26,17 +26,23 @@ export default class Trigger { private stop: () => void; public constructor( - trigger: Trigger["trigger"], - options: Trigger["options"] | Trigger["_script"], - script?: Trigger["_script"] + trigger: symbol, + options: TriggerOptions, + script?: GeneratorFunction + ); + public constructor(trigger: symbol, script: GeneratorFunction); + public constructor( + trigger: symbol, + optionsOrScript: TriggerOptions | GeneratorFunction, + script?: GeneratorFunction ) { this.trigger = trigger; if (typeof script === "undefined") { this.options = {}; - this._script = options as Trigger["_script"]; + this._script = optionsOrScript as GeneratorFunction; } else { - this.options = options as Trigger["options"]; + this.options = optionsOrScript as TriggerOptions; this._script = script; } From 1d9cc7fdb7c7259ac39dbfbb0780ac254916ff23 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 14/46] Clarify _getSkin signature --- src/Renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index 6eda197..1bbc8c6 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -125,7 +125,7 @@ export default class Renderer { } // Retrieve a given object (e.g. costume or speech bubble)'s skin. If it doesn't exist, make one. - public _getSkin(obj: object): Skin { + public _getSkin(obj: Costume | SpeechBubble): Skin { const existingSkin = this._skins.get(obj); if (existingSkin) return existingSkin; @@ -139,7 +139,7 @@ export default class Renderer { } } else { // If it's not a costume, assume it's a speech bubble. - skin = new SpeechBubbleSkin(this, obj as SpeechBubble); + skin = new SpeechBubbleSkin(this, obj); } this._skins.set(obj, skin); return skin; From 5cdab2a5b9d90de0388ccaaee180d5c1e45184cf Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 15/46] Swap _setShader if/else --- src/Renderer.ts | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index 1bbc8c6..3594b00 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -226,32 +226,30 @@ export default class Renderer { } public _setShader(shader: Shader): boolean { - if (shader !== this._currentShader) { - const gl = this.gl; - gl.useProgram(shader.program); - - // These attributes and uniforms don't ever change, but must be set whenever a new shader program is used. - - const attribLocation = shader.attribs.a_position; - gl.enableVertexAttribArray(attribLocation); - // Bind the 'a_position' vertex attribute to the current contents of `gl.ARRAY_BUFFER`, which in this case - // is a quadrilateral (as buffered earlier). - gl.vertexAttribPointer( - attribLocation, - 2, // every 2 array elements make one vertex. - gl.FLOAT, // data type - false, // normalized - 0, // stride (space between attributes) - 0 // offset (index of the first attribute to start from) - ); + if (shader === this._currentShader) return false; - this._currentShader = shader; - this._updateStageSize(); + const gl = this.gl; + gl.useProgram(shader.program); + + // These attributes and uniforms don't ever change, but must be set whenever a new shader program is used. + + const attribLocation = shader.attribs.a_position; + gl.enableVertexAttribArray(attribLocation); + // Bind the 'a_position' vertex attribute to the current contents of `gl.ARRAY_BUFFER`, which in this case + // is a quadrilateral (as buffered earlier). + gl.vertexAttribPointer( + attribLocation, + 2, // every 2 array elements make one vertex. + gl.FLOAT, // data type + false, // normalized + 0, // stride (space between attributes) + 0 // offset (index of the first attribute to start from) + ); - return true; - } + this._currentShader = shader; + this._updateStageSize(); - return false; + return true; } public _setFramebuffer(framebufferInfo: FramebufferInfo | null): void { From 265aacdea35c9e6b136afaf66253c520ca40c513 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 16/46] Replace typeof with truthiness check --- src/Renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index 3594b00..3b9d0b8 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -303,7 +303,7 @@ export default class Renderer { const shouldIncludeLayer = (layer: Sprite | Stage | PenSkin): boolean => !( (shouldRestrictLayers && !layers.has(layer)) || - (typeof options.filter === "function" && !options.filter(layer)) + (options.filter && !options.filter(layer)) ); // Stage From 8646b8a84ec3e2b553ad366fd6fe675df6afbc7b Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 17/46] Make createStage private and static --- src/Renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index 3b9d0b8..51b0854 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -71,7 +71,7 @@ export default class Renderer { const w = project.stage.width; const h = project.stage.height; this.project = project; - this.stage = this.createStage(w, h); + this.stage = Renderer.createStage(w, h); const gl = this.stage.getContext("webgl", { antialias: false }); if (gl === null) throw new Error("Could not initialize WebGL context"); this.gl = gl; @@ -395,7 +395,7 @@ export default class Renderer { this._renderLayers(); } - public createStage(w: number, h: number): HTMLCanvasElement { + private static createStage(w: number, h: number): HTMLCanvasElement { const stage = document.createElement("canvas"); stage.width = w; stage.height = h; From e570ca4659aff906a56969ffa81323f25785edcc Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 18/46] export RGBA/RGBANormalized types --- src/Color.ts | 14 ++++++++++++-- src/Renderer.ts | 7 ++++--- src/renderer/PenSkin.ts | 3 ++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Color.ts b/src/Color.ts index 11eff0f..4d1dd22 100644 --- a/src/Color.ts +++ b/src/Color.ts @@ -90,6 +90,16 @@ function hsvToRGB( }; } +/** + * RGBA color, with each component going from 0 to 255. Components may still be decimal. + */ +export type RGBA = [number, number, number, number]; + +/** + * RGBA color, with each component going from 0 to 1. + */ +export type RGBANormalized = [number, number, number, number]; + export default class Color { private _h = 0; private _s = 0; @@ -221,12 +231,12 @@ export default class Color { return `rgb(${rgb.join(", ")})`; } - public toRGBA(): [number, number, number, number] { + public toRGBA(): RGBA { const rgb = hsvToRGB(this._h, this._s, this._v); return [rgb.r, rgb.g, rgb.b, this._a * 255]; } - public toRGBANormalized(): [number, number, number, number] { + public toRGBANormalized(): RGBANormalized { const rgb = hsvToRGB(this._h, this._s, this._v); return [rgb.r / 255, rgb.g / 255, rgb.b / 255, this._a]; } diff --git a/src/Renderer.ts b/src/Renderer.ts index 51b0854..0f3b70e 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -11,6 +11,7 @@ import type Skin from "./renderer/Skin"; import Costume from "./Costume"; import type Color from "./Color"; +import type { RGBANormalized } from "./Color"; import type Project from "./Project"; import { Sprite, Stage, _EffectMap, SpeechBubble } from "./Sprite"; @@ -36,7 +37,7 @@ const colorToId = ([r, g, b]: [number, number, number] | Uint8Array): number => type RenderSpriteOptions = { drawMode: DrawMode; effectMask?: number; - colorMask?: [number, number, number, number]; + colorMask?: RGBANormalized; renderSpeechBubbles?: boolean; spriteColorId?: (target: Sprite | Stage) => number; }; @@ -448,7 +449,7 @@ export default class Renderer { scale: number, effects?: _EffectMap, effectMask?: number, - colorMask?: [number, number, number, number], + colorMask?: RGBANormalized, spriteColorId?: number ): void { const gl = this.gl; @@ -572,7 +573,7 @@ export default class Renderer { drawMode: DrawMode; renderSpeechBubbles: boolean; effectMask: number; - colorMask?: [number, number, number, number]; + colorMask?: RGBANormalized; } & RenderSpriteOptions = { drawMode: ShaderManager.DrawModes.SILHOUETTE, renderSpeechBubbles: false, diff --git a/src/renderer/PenSkin.ts b/src/renderer/PenSkin.ts index bbac7de..acc6002 100644 --- a/src/renderer/PenSkin.ts +++ b/src/renderer/PenSkin.ts @@ -1,6 +1,7 @@ import Skin from "./Skin"; import ShaderManager from "./ShaderManager"; import type Color from "../Color"; +import type { RGBANormalized } from "../Color"; import type Renderer from "../Renderer"; import type { FramebufferInfo } from "../Renderer"; @@ -8,7 +9,7 @@ export default class PenSkin extends Skin { public _framebufferInfo: FramebufferInfo; private _lastPenState: { size: number; - color: [number, number, number, number]; + color: RGBANormalized; }; public constructor(renderer: Renderer, width: number, height: number) { From 6b4ef480fd1bf1cb4fcd117057f14655704c4df4 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 19/46] Allow checkPointCollision for Stage --- src/Renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index 0f3b70e..910c1ee 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -783,11 +783,11 @@ export default class Renderer { } public checkPointCollision( - spr: Sprite, + spr: Sprite | Stage, point: { x: number; y: number }, fast?: boolean ): boolean { - if (!spr.visible) return false; + if ("visible" in spr && !spr.visible) return false; const box = this.getBoundingBox(spr); if (!box.containsPoint(point.x, point.y)) return false; From 4fcebf7b5b64293f2ae3936439253ce8fd79497f Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 20/46] Be more consistent with Sound field types --- src/Sound.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sound.ts b/src/Sound.ts index 91a8c7f..74bb108 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -8,10 +8,10 @@ export default class Sound { private audioBuffer: AudioBuffer | null; private source: AudioBufferSourceNode | null; private playbackRate: number; - private target?: AudioNode; + private target: AudioNode | undefined; - private _markDone?: () => void; - private _doneDownloading?: (fromMoreRecentCall: boolean) => void; + private _markDone: (() => void) | undefined; + private _doneDownloading: ((fromMoreRecentCall: boolean) => void) | undefined; private static _audioContext: AudioContext | undefined; public constructor(name: string, url: string) { From 59ae16c9483c4f15e3a720141c8c4820b88a9637 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 21/46] Clean up effect descriptor types + docs --- src/Sound.ts | 73 +++++++++++++++++++++++++++------------------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/src/Sound.ts b/src/Sound.ts index 74bb108..d0cdd35 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -242,6 +242,42 @@ export default class Sound { // add more audio effects in the future. (Scratch used to have more, but they // were removed - see commit ff6cd4a - because they depended on an external // library and were too processor-intensive to support on some devices.) + +type EffectDescriptorBase = { + name: Name; + initial: number; + minimum?: number; + maximum?: number; + resetOnStart?: boolean; + resetOnClone?: boolean; +}; + +type PatchlessDescriptor = { + isPatch: false; + set: (value: number, sound: Sound) => void; +} & EffectDescriptorBase; + +type PatchDescriptor = { + isPatch: true; + makeNodes: () => Nodes & { input: AudioNode; output: AudioNode }; + set: ( + value: number, + nodes: Nodes & { input: AudioNode; output: AudioNode } + ) => void; +} & EffectDescriptorBase; + +type EffectDescriptor< + isPatch extends boolean, + Name extends string, + Nodes extends isPatch extends true ? object : never +> = isPatch extends true + ? PatchDescriptor + : PatchlessDescriptor; + +type Effects = { + [x in EffectName]: number; +}; + const PanEffect: EffectDescriptor< true, "pan", @@ -283,6 +319,7 @@ const PanEffect: EffectDescriptor< ); }, } as const; + const PitchEffect: EffectDescriptor = { name: "pitch", initial: 0, @@ -293,6 +330,7 @@ const PitchEffect: EffectDescriptor = { sound.setPlaybackRate(ratio); }, } as const; + const VolumeEffect: EffectDescriptor = { name: "volume", initial: 100, @@ -705,41 +743,6 @@ export class EffectChain { public static effectDescriptors = effectDescriptors; } -type EffectDescriptorBase = { - name: Name; - initial: number; - minimum?: number; - maximum?: number; - resetOnStart?: boolean; - resetOnClone?: boolean; -}; - -type PatchlessDescriptor = { - isPatch: false; - set: (value: number, sound: Sound) => void; -} & EffectDescriptorBase; - -type PatchDescriptor = { - isPatch: true; - makeNodes: () => Nodes & { input: AudioNode; output: AudioNode }; - set: ( - value: number, - nodes: Nodes & { input: AudioNode; output: AudioNode } - ) => void; -} & EffectDescriptorBase; - -type EffectDescriptor< - isPatch extends boolean, - Name extends string, - Nodes extends isPatch extends true ? object : never -> = isPatch extends true - ? PatchDescriptor - : PatchlessDescriptor; - -type Effects = { - [x in EffectName]: number; -}; - export class AudioEffectMap implements Effects { // This class provides a simple interface for setting and getting audio // effects stored on an EffectChain, similar to EffectMap (that class being From b78841290685ec8aa1a5a283cfccdfe7a2fdb448 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 22/46] Remove EffectChain.config --- src/Sound.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Sound.ts b/src/Sound.ts index d0cdd35..54968f4 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -367,8 +367,6 @@ export class EffectChain { // a portable way to store the effect chain, independent of the audio sources // it affects. - // TODO: stop storing config; we just use getNonPatchSoundList directly - private config: EffectChainConfig; public inputNode: AudioNode; private getNonPatchSoundList: () => Sound[]; private effectValues!: Record; @@ -383,7 +381,6 @@ export class EffectChain { public constructor(config: EffectChainConfig) { const { getNonPatchSoundList } = config; - this.config = config; this.inputNode = Sound.audioContext.createGain(); @@ -637,9 +634,7 @@ export class EffectChain { } public clone(newConfig: EffectChainConfig): EffectChain { - const newEffectChain = new EffectChain( - Object.assign({}, this.config, newConfig) - ); + const newEffectChain = new EffectChain({getNonPatchSoundList: this.getNonPatchSoundList}); for (const [name, value] of Object.entries(this.effectValues) as [ EffectName, From d761d127b25f66f36a690b2ab794e336942f9155 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 23/46] Remove "infer" from effectNodes type --- src/Sound.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Sound.ts b/src/Sound.ts index 54968f4..ea9dbd6 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -372,9 +372,7 @@ export class EffectChain { private effectValues!: Record; private effectNodes: { [T in typeof effectDescriptors[number] as T["name"]]?: ReturnType< - // We need to infer this type here, I think - // eslint-disable-next-line @typescript-eslint/no-unused-vars - T extends PatchDescriptor ? T["makeNodes"] : never + T extends PatchDescriptor ? T["makeNodes"] : never >; }; private target?: AudioNode; From f7c35acf450cdf3da1ee14c1b4e62be67bc79ad9 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 24/46] Move Trigger symbols to Trigger class --- src/Trigger.ts | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Trigger.ts b/src/Trigger.ts index 40718ec..1165287 100644 --- a/src/Trigger.ts +++ b/src/Trigger.ts @@ -1,14 +1,5 @@ import type { Sprite, Stage } from "./Sprite"; -const GREEN_FLAG = Symbol("GREEN_FLAG"); -const KEY_PRESSED = Symbol("KEY_PRESSED"); -const BROADCAST = Symbol("BROADCAST"); -const CLICKED = Symbol("CLICKED"); -const CLONE_START = Symbol("CLONE_START"); -const LOUDNESS_GREATER_THAN = Symbol("LOUDNESS_GREATER_THAN"); -const TIMER_GREATER_THAN = Symbol("TIMER_GREATER_THAN"); -const BACKDROP_CHANGED = Symbol("BACKDROP_CHANGED"); - type TriggerOption = | number | string @@ -53,8 +44,8 @@ export default class Trigger { public get isEdgeActivated(): boolean { return ( - this.trigger === TIMER_GREATER_THAN || - this.trigger === LOUDNESS_GREATER_THAN + this.trigger === Trigger.TIMER_GREATER_THAN || + this.trigger === Trigger.LOUDNESS_GREATER_THAN ); } @@ -112,14 +103,16 @@ export default class Trigger { return new Trigger(this.trigger, this.options, this._script); } - public static GREEN_FLAG = GREEN_FLAG; - public static KEY_PRESSED = KEY_PRESSED; - public static BROADCAST = BROADCAST; - public static CLICKED = CLICKED; - public static CLONE_START = CLONE_START; - public static LOUDNESS_GREATER_THAN = LOUDNESS_GREATER_THAN; - public static TIMER_GREATER_THAN = TIMER_GREATER_THAN; - public static BACKDROP_CHANGED = BACKDROP_CHANGED; + public static readonly GREEN_FLAG = Symbol("GREEN_FLAG"); + public static readonly KEY_PRESSED = Symbol("KEY_PRESSED"); + public static readonly BROADCAST = Symbol("BROADCAST"); + public static readonly CLICKED = Symbol("CLICKED"); + public static readonly CLONE_START = Symbol("CLONE_START"); + public static readonly LOUDNESS_GREATER_THAN = Symbol( + "LOUDNESS_GREATER_THAN" + ); + public static readonly TIMER_GREATER_THAN = Symbol("TIMER_GREATER_THAN"); + public static readonly BACKDROP_CHANGED = Symbol("BACKDROP_CHANGED"); } export type { TriggerOption, TriggerOptions }; From d1ae3f6cb9e8ff16d9bb10a13a308c966f903e15 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 25/46] Fix "as never" cast --- src/Sound.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Sound.ts b/src/Sound.ts index ea9dbd6..684b4a4 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -498,7 +498,9 @@ export class EffectChain { let nodes = this.effectNodes[descriptor.name]!; if (!nodes && value !== descriptor.initial) { nodes = descriptor.makeNodes(); - this.effectNodes[descriptor.name] = nodes as never; + // The "as any" cast is needed because TypeScript can't infer that the + // descriptor's name determines the type of its nodes + this.effectNodes[descriptor.name] = nodes as any; // Connect the previous effect, or, if there is none, the EffectChain // input, to this effect. Also disconnect it from whatever it was @@ -544,7 +546,9 @@ export class EffectChain { delete this.effectNodes[name]; } } else { - descriptor.set(value, nodes as never); + // The "as any" cast is needed because TypeScript can't infer that the + // descriptor's name determines the type of its nodes + descriptor.set(value, nodes as any); } } else { // Non-"patch" effects operate directly on Sound objects, accessing From c6d13370a25b6ea11256fced78b4656cc6733c1b Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 26/46] Remove boundScript --- src/Trigger.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Trigger.ts b/src/Trigger.ts index 1165287..98a82d7 100644 --- a/src/Trigger.ts +++ b/src/Trigger.ts @@ -80,10 +80,8 @@ export default class Trigger { public start(target: Sprite | Stage): Promise { this.stop(); - const boundScript = this._script.bind(target); - this.done = false; - this._runningScript = boundScript(); + this._runningScript = this._script.call(target); return new Promise((resolve) => { this.stop = (): void => { From a2dfc903c5b1ec89a80e83258ace237d4f017d26 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 27/46] Remove vars default parameter --- src/Sprite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index d4a5993..c1a0060 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -133,7 +133,7 @@ abstract class SpriteBase { this.effects = new _EffectMap(); this.audioEffects = new AudioEffectMap(this.effectChain); - this._vars = vars ?? {}; + this._vars = vars; } protected getSoundsPlayedByMe(): Sound[] { From c51ff884736507189a9e8459c8c88f5c44986655 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 28/46] Override Stage costumeNumber --- src/Sprite.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Sprite.ts b/src/Sprite.ts index c1a0060..1406f36 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -903,4 +903,13 @@ export class Stage extends SpriteBase { backdrop: this.costume.name, }); } + + public get costumeNumber(): number { + return super.costumeNumber; + } + + public set costumeNumber(number) { + super.costumeNumber = number; + void this.fireBackdropChanged(); + } } From 1c94a379ff543475202ded035592f8f100d97040 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 29/46] Make costumeNumber match Scratch --- src/Sprite.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index 1406f36..9d29210 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -157,7 +157,11 @@ abstract class SpriteBase { } public set costumeNumber(number) { - this._costumeNumber = this.wrapClamp(number, 1, this.costumes.length); + if (Number.isFinite(number)) { + this._costumeNumber = this.wrapClamp(number, 1, this.costumes.length); + } else { + this._costumeNumber = 0; + } } public set costume(costume: number | string | Costume) { @@ -168,7 +172,7 @@ abstract class SpriteBase { } } if (typeof costume === "number") { - if (!isNaN(costume)) this.costumeNumber = costume; + this.costumeNumber = costume; return; } if (typeof costume === "string") { @@ -208,10 +212,7 @@ abstract class SpriteBase { } default: { - if ( - Number.isFinite(Number(costume)) && - costume.trim().length !== 0 - ) { + if (!Number.isNaN(Number(costume)) && costume.trim().length !== 0) { this.costumeNumber = Number(costume); } } From 2855921f82687ae628d693166adfdfef2bf96a86 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 30/46] Override askAndWait --- src/Sprite.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index 9d29210..514b3cc 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -110,8 +110,6 @@ abstract class SpriteBase { public effects: _EffectMap; public audioEffects: AudioEffectMap; - public _speechBubble?: SpeechBubble; - protected _vars: Vars; public constructor(initialConditions: InitialConditions, vars: Vars) { @@ -380,10 +378,6 @@ abstract class SpriteBase { } public *askAndWait(question: string): Yielding { - if (this._speechBubble && this instanceof Sprite) { - this.say(""); - } - let done = false; void this._project.askAndWait(question).then(() => { done = true; @@ -533,6 +527,7 @@ export class Sprite extends SpriteBase { private _penDown: boolean; public penSize: number; private _penColor: Color; + public _speechBubble?: SpeechBubble; public constructor(initialConditions: SpriteInitialConditions, vars: Vars) { super(initialConditions, vars); @@ -572,6 +567,14 @@ export class Sprite extends SpriteBase { }; } + public *askAndWait(question: string): Yielding { + if (this._speechBubble && this instanceof Sprite) { + this.say(""); + } + + yield* super.askAndWait(question); + } + public createClone(): void { const clone = Object.assign( Object.create(Object.getPrototypeOf(this) as object) as Sprite, From 2f13eb2e93e112481d165207acc2c256e9f84833 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 31/46] Remove TODO --- src/Sprite.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index 514b3cc..896072d 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -632,7 +632,6 @@ export class Sprite extends SpriteBase { ); } - // TODO: is this necessary now that the clone hierarchy seems to be flattened? public andClones(): Sprite[] { return [this, ...this.clones.flatMap((clone) => clone.andClones())]; } From eb595d16bb510aef20891ab04ede3e89adaaa60d Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 32/46] Type moveAhead/moveBehind better --- src/Sprite.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index 896072d..857e366 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -701,7 +701,7 @@ export class Sprite extends SpriteBase { } while (t < 1); } - public moveAhead(value = Infinity): void { + public moveAhead(value: number | Sprite = Infinity): void { if (typeof value === "number") { this._project.changeSpriteLayer(this, value); } else { @@ -709,7 +709,7 @@ export class Sprite extends SpriteBase { } } - public moveBehind(value = Infinity): void { + public moveBehind(value: number | Sprite = Infinity): void { if (typeof value === "number") { this._project.changeSpriteLayer(this, -value); } else { From 31bff8225f9b62cfe6c3b31a5ac5762da9988e11 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 33/46] Add extra _speechBubble check to fix typing --- src/Renderer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Renderer.ts b/src/Renderer.ts index 910c1ee..d699168 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -521,6 +521,7 @@ export default class Renderer { if ( options.renderSpeechBubbles && + '_speechBubble' in sprite && sprite._speechBubble && sprite._speechBubble.text !== "" && sprite instanceof Sprite From b2aaf7936307ea5eddefe3d3be0301b605e11d16 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 34/46] Document getTexture/getImageData --- src/renderer/PenSkin.ts | 4 ++++ src/renderer/Skin.ts | 15 +++++++++------ src/renderer/SpeechBubbleSkin.ts | 10 ++++++++++ 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/renderer/PenSkin.ts b/src/renderer/PenSkin.ts index acc6002..2a4b6df 100644 --- a/src/renderer/PenSkin.ts +++ b/src/renderer/PenSkin.ts @@ -42,6 +42,10 @@ export default class PenSkin extends Skin { return this._framebufferInfo.texture; } + public getImageData(): ImageData | null { + return null; + } + public penLine( pt1: { x: number; y: number }, pt2: { x: number; y: number }, diff --git a/src/renderer/Skin.ts b/src/renderer/Skin.ts index 3b7eb87..0f29528 100644 --- a/src/renderer/Skin.ts +++ b/src/renderer/Skin.ts @@ -13,14 +13,17 @@ export default abstract class Skin { this.height = 0; } - // Get the skin's texture for a given (screen-space) scale. + /** + * Get the skin's texture at a given screen-space scale. + * @param scale The screen-space scale factor for the texture, as a ratio of screen pixels to texture pixels. + */ public abstract getTexture(scale: number): WebGLTexture | null; - // Get the skin image's ImageData at a given (screen-space) scale. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public getImageData(scale: number): ImageData | null { - throw new Error("getImageData not implemented for this skin type"); - } + /** + * Gets the raster ImageData for a skin's texture at a given screen-space scale. + * @param scale The screen-space scale factor for the texture, as a ratio of screen pixels to texture pixels. + */ + public abstract getImageData(scale: number): ImageData | null; // Helper function to create a texture from an image and handle all the boilerplate. protected _makeTexture( diff --git a/src/renderer/SpeechBubbleSkin.ts b/src/renderer/SpeechBubbleSkin.ts index 917885e..ad4acca 100644 --- a/src/renderer/SpeechBubbleSkin.ts +++ b/src/renderer/SpeechBubbleSkin.ts @@ -162,6 +162,16 @@ export default class SpeechBubbleSkin extends Skin { return this._texture; } + public getImageData(scale: number): ImageData | null { + this.getTexture(scale); + return this._ctx.getImageData( + 0, + 0, + this._canvas.width, + this._canvas.height + ); + } + public destroy(): void { this.gl.deleteTexture(this._texture); } From d833ef6dbd29af247aad421d6f0a5aa493ff8fd5 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:22 -0500 Subject: [PATCH 35/46] Add flipped getter --- src/renderer/SpeechBubbleSkin.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/SpeechBubbleSkin.ts b/src/renderer/SpeechBubbleSkin.ts index ad4acca..4865679 100644 --- a/src/renderer/SpeechBubbleSkin.ts +++ b/src/renderer/SpeechBubbleSkin.ts @@ -48,7 +48,11 @@ export default class SpeechBubbleSkin extends Skin { ctx.textBaseline = "hanging"; } - public set flipped(flipped: boolean) { + public get flipped(): boolean { + return this._flipped; + } + + public set flipped(flipped) { this._flipped = flipped; this._rendered = false; } From f3477974605614930d3f3e970222ae39d2349c0c Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:23 -0500 Subject: [PATCH 36/46] SpeechBubbleStyle type --- src/Sprite.ts | 4 +++- src/renderer/SpeechBubbleSkin.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index 857e366..df69b3d 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -83,9 +83,11 @@ export class _EffectMap implements Effects { } } +export type SpeechBubbleStyle = "say" | "think"; + export type SpeechBubble = { text: string; - style: "say" | "think"; + style: SpeechBubbleStyle; timeout: number | null; }; diff --git a/src/renderer/SpeechBubbleSkin.ts b/src/renderer/SpeechBubbleSkin.ts index 4865679..bb2f9ae 100644 --- a/src/renderer/SpeechBubbleSkin.ts +++ b/src/renderer/SpeechBubbleSkin.ts @@ -1,6 +1,6 @@ import Skin from "./Skin"; import type Renderer from "../Renderer"; -import type { SpeechBubble } from "../Sprite"; +import type { SpeechBubble, SpeechBubbleStyle } from "../Sprite"; const bubbleStyle = { maxLineWidth: 170, @@ -67,7 +67,7 @@ export default class SpeechBubbleSkin extends Skin { w: number, h: number, r: number, - style: "say" | "think" + style: SpeechBubbleStyle ): void => { if (r > w / 2) r = w / 2; if (r > h / 2) r = h / 2; From 30b1a7359d3d0e5ad1b94449f9481779f2a6aa3f Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:23 -0500 Subject: [PATCH 37/46] Fix unnecessary initialized assertions --- src/renderer/Drawable.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/Drawable.ts b/src/renderer/Drawable.ts index 89f7f00..de28a10 100644 --- a/src/renderer/Drawable.ts +++ b/src/renderer/Drawable.ts @@ -27,11 +27,11 @@ class SpriteTransformDiff { private _sprite: Sprite | Stage; private _unset: boolean; - private _lastX!: Sprite["x"] | undefined; - private _lastY!: Sprite["y"] | undefined; - private _lastRotation!: Sprite["direction"] | undefined; - private _lastRotationStyle!: Sprite["rotationStyle"] | undefined; - private _lastSize!: Sprite["size"] | undefined; + private _lastX: Sprite["x"] | undefined; + private _lastY: Sprite["y"] | undefined; + private _lastRotation: Sprite["direction"] | undefined; + private _lastRotationStyle: Sprite["rotationStyle"] | undefined; + private _lastSize: Sprite["size"] | undefined; private _lastCostume!: Sprite["costume"]; private _lastCostumeLoaded!: boolean; From 160bb9ec555e17f6728c99b2f252508d7a918eaf Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Tue, 7 Mar 2023 21:43:23 -0500 Subject: [PATCH 38/46] Fix watcher min/max/step init --- src/Watcher.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Watcher.ts b/src/Watcher.ts index 2c7ff61..389d82f 100644 --- a/src/Watcher.ts +++ b/src/Watcher.ts @@ -22,6 +22,8 @@ type WatcherOptions = { y?: number; width?: number; height?: number; + min?: number; + max?: number; }; export default class Watcher { @@ -81,9 +83,6 @@ export default class Watcher { this.y = y; this.width = width; this.height = height; - this.min = 0; - this.max = 100; - this.step = 1; } private initializeDOM(): void { From 334e8308aeac20de8db3096a1f0a305bdb15c6b4 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Wed, 8 Mar 2023 12:38:50 -0500 Subject: [PATCH 39/46] Move filter param --- src/Renderer.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Renderer.ts b/src/Renderer.ts index d699168..076e11b 100644 --- a/src/Renderer.ts +++ b/src/Renderer.ts @@ -287,13 +287,11 @@ export default class Renderer { // Handles rendering of all layers (including stage, pen layer, sprites, and all clones) in proper order. private _renderLayers( layers?: Set, - optionsIn: { - filter?: (layer: Sprite | Stage | PenSkin) => boolean; - } & Partial = {} + optionsIn: Partial = {}, + filter?: (layer: Sprite | Stage | PenSkin) => boolean ): void { const options = { drawMode: ShaderManager.DrawModes.DEFAULT, - renderSpeechBubbles: true, ...optionsIn, }; @@ -304,7 +302,7 @@ export default class Renderer { const shouldIncludeLayer = (layer: Sprite | Stage | PenSkin): boolean => !( (shouldRestrictLayers && !layers.has(layer)) || - (options.filter && !options.filter(layer)) + (filter && !filter(layer)) ); // Stage @@ -520,8 +518,8 @@ export default class Renderer { ); if ( - options.renderSpeechBubbles && - '_speechBubble' in sprite && + options.renderSpeechBubbles !== false && + "_speechBubble" in sprite && sprite._speechBubble && sprite._speechBubble.text !== "" && sprite instanceof Sprite @@ -708,9 +706,7 @@ export default class Renderer { this._stencilSprite(spr, sprColor); // Render the sprites to check that we're touching, which will now be masked in to the area of the first sprite. - this._renderLayers(undefined, { - filter: (layer) => layer !== spr, - }); + this._renderLayers(undefined, undefined, (layer) => layer !== spr); // Make sure to disable the stencil test so as not to affect other rendering! gl.disable(gl.STENCIL_TEST); From 24f897c9207222dc0ded43f864e475da387b3d9c Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Wed, 8 Mar 2023 12:48:28 -0500 Subject: [PATCH 40/46] Make _project and _layerOrder protected --- src/Sprite.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index df69b3d..ecc9961 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -97,12 +97,10 @@ type InitialConditions = { }; abstract class SpriteBase { - // TODO: make private - public _project!: Project; + protected _project!: Project; protected _costumeNumber: number; - // TODO: make private - public _layerOrder: number; + protected _layerOrder: number; public triggers: Trigger[]; public watchers: Partial>; protected costumes: Costume[]; From 0bbab06a886ce9750e85d095384290ee4cb3188d Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Mar 2023 13:00:58 -0500 Subject: [PATCH 41/46] Remove vars type parameter --- src/Sprite.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index ecc9961..fb76383 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -96,7 +96,7 @@ type InitialConditions = { layerOrder?: number; }; -abstract class SpriteBase { +abstract class SpriteBase { protected _project!: Project; protected _costumeNumber: number; @@ -110,9 +110,9 @@ abstract class SpriteBase { public effects: _EffectMap; public audioEffects: AudioEffectMap; - protected _vars: Vars; + protected _vars: object; - public constructor(initialConditions: InitialConditions, vars: Vars) { + public constructor(initialConditions: InitialConditions, vars = {}) { // TODO: pass project in here, ideally const { costumeNumber, layerOrder = 0 } = initialConditions; this._costumeNumber = costumeNumber; @@ -146,7 +146,7 @@ abstract class SpriteBase { return this._project.sprites; } - public get vars(): Vars { + public get vars(): object { return this._vars; } @@ -513,7 +513,7 @@ type SpriteInitialConditions = { penColor?: Color; }; -export class Sprite extends SpriteBase { +export class Sprite extends SpriteBase { private _x: number; private _y: number; private _direction: number; @@ -522,14 +522,14 @@ export class Sprite extends SpriteBase { public visible: boolean; private parent: Sprite | null; - public clones: Sprite[]; + public clones: Sprite[]; private _penDown: boolean; public penSize: number; private _penColor: Color; public _speechBubble?: SpeechBubble; - public constructor(initialConditions: SpriteInitialConditions, vars: Vars) { + public constructor(initialConditions: SpriteInitialConditions, vars = {}) { super(initialConditions, vars); const { @@ -876,12 +876,12 @@ type StageInitialConditions = { height?: number; } & InitialConditions; -export class Stage extends SpriteBase { +export class Stage extends SpriteBase { public readonly width!: number; public readonly height!: number; public __counter: number; - public constructor(initialConditions: StageInitialConditions, vars: Vars) { + public constructor(initialConditions: StageInitialConditions, vars = {}) { super(initialConditions, vars); // Use defineProperties to make these non-writable. From df9450641108ba7ce9e1d8d7f5041bc732bf87f1 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Mar 2023 13:04:13 -0500 Subject: [PATCH 42/46] Make clone methods return subclass type --- src/Sprite.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index fb76383..6b25d0d 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -521,8 +521,8 @@ export class Sprite extends SpriteBase { public size: number; public visible: boolean; - private parent: Sprite | null; - public clones: Sprite[]; + private parent: this | null; + public clones: this[]; private _penDown: boolean; public penSize: number; @@ -577,7 +577,7 @@ export class Sprite extends SpriteBase { public createClone(): void { const clone = Object.assign( - Object.create(Object.getPrototypeOf(this) as object) as Sprite, + Object.create(Object.getPrototypeOf(this) as object) as this, this ); @@ -598,7 +598,7 @@ export class Sprite extends SpriteBase { // Clones inherit audio effects from the original sprite, for some reason. // Couldn't explain it, but that's the behavior in Scratch 3.0. // eslint-disable-next-line @typescript-eslint/no-this-alias - let original: Sprite = this; + let original = this; while (original.parent) { original = original.parent; } @@ -632,7 +632,7 @@ export class Sprite extends SpriteBase { ); } - public andClones(): Sprite[] { + public andClones(): this[] { return [this, ...this.clones.flatMap((clone) => clone.andClones())]; } From 10ff7c534b791c8f437b49a1a66b4877ccb03657 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Mar 2023 13:04:39 -0500 Subject: [PATCH 43/46] Remove unnecessary instanceof --- src/Sprite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sprite.ts b/src/Sprite.ts index 6b25d0d..104001e 100644 --- a/src/Sprite.ts +++ b/src/Sprite.ts @@ -568,7 +568,7 @@ export class Sprite extends SpriteBase { } public *askAndWait(question: string): Yielding { - if (this._speechBubble && this instanceof Sprite) { + if (this._speechBubble) { this.say(""); } From 61a7c09ff83cfb8e21ccc5b5b0abbaf88c5c0905 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Mar 2023 14:17:05 -0500 Subject: [PATCH 44/46] Format + lint --- src/Sound.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Sound.ts b/src/Sound.ts index 684b4a4..6ea767c 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -372,6 +372,7 @@ export class EffectChain { private effectValues!: Record; private effectNodes: { [T in typeof effectDescriptors[number] as T["name"]]?: ReturnType< + // eslint-disable-next-line @typescript-eslint/no-explicit-any T extends PatchDescriptor ? T["makeNodes"] : never >; }; @@ -500,6 +501,7 @@ export class EffectChain { nodes = descriptor.makeNodes(); // The "as any" cast is needed because TypeScript can't infer that the // descriptor's name determines the type of its nodes + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any this.effectNodes[descriptor.name] = nodes as any; // Connect the previous effect, or, if there is none, the EffectChain @@ -548,6 +550,7 @@ export class EffectChain { } else { // The "as any" cast is needed because TypeScript can't infer that the // descriptor's name determines the type of its nodes + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument descriptor.set(value, nodes as any); } } else { @@ -636,7 +639,9 @@ export class EffectChain { } public clone(newConfig: EffectChainConfig): EffectChain { - const newEffectChain = new EffectChain({getNonPatchSoundList: this.getNonPatchSoundList}); + const newEffectChain = new EffectChain({ + getNonPatchSoundList: this.getNonPatchSoundList, + }); for (const [name, value] of Object.entries(this.effectValues) as [ EffectName, From 9c29df5c62148ae6d262437ae4b6b9c8f03d9e71 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Thu, 9 Mar 2023 14:17:36 -0500 Subject: [PATCH 45/46] Fix Sound.clone --- src/Sound.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Sound.ts b/src/Sound.ts index 6ea767c..74f4142 100644 --- a/src/Sound.ts +++ b/src/Sound.ts @@ -639,9 +639,14 @@ export class EffectChain { } public clone(newConfig: EffectChainConfig): EffectChain { - const newEffectChain = new EffectChain({ - getNonPatchSoundList: this.getNonPatchSoundList, - }); + const newEffectChain = new EffectChain( + Object.assign( + { + getNonPatchSoundList: this.getNonPatchSoundList, + }, + newConfig + ) + ); for (const [name, value] of Object.entries(this.effectValues) as [ EffectName, From 4ccf1b294b2ba353a60f0c6423c4b317a7cda977 Mon Sep 17 00:00:00 2001 From: adroitwhiz Date: Fri, 10 Mar 2023 14:51:34 -0500 Subject: [PATCH 46/46] Bump Node to 18 --- .github/workflows/npm-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 80a6b81..f1a4b9e 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -14,7 +14,7 @@ jobs: # - uses: actions/checkout@v3 # - uses: actions/setup-node@v3 # with: - # node-version: 16 + # node-version: 18 # - run: npm ci # - run: npm test @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm publish