From 3dbabfbf0c8eb899050e70047ded0173c9970d9a Mon Sep 17 00:00:00 2001 From: kozhilya Date: Mon, 1 Apr 2024 00:48:19 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A8=D1=83=D1=82=D0=BA=D0=B0=20"=D0=A1=D0=B2?= =?UTF-8?q?=D0=B5=D0=B6=D0=B0=D1=8F=20=D1=82=D1=80=D0=B0=D0=B2=D0=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/april-fools.js | 41 +++- settings.compact.js | 17 ++ settings.example.js | 63 ++++++ src/common.ts | 64 +++++- src/html/joke-fresh_glass.html | 16 ++ src/html/styles.html | 190 +++++++++++------ src/jokes/fresh_grass.ts | 362 +++++++++++++++++++++++++++++++++ src/lib/vector.ts | 150 ++++++++++++++ src/main.ts | 2 + 9 files changed, 836 insertions(+), 69 deletions(-) create mode 100644 src/html/joke-fresh_glass.html create mode 100644 src/jokes/fresh_grass.ts create mode 100644 src/lib/vector.ts diff --git a/dist/april-fools.js b/dist/april-fools.js index cf13a98..7ea765f 100644 --- a/dist/april-fools.js +++ b/dist/april-fools.js @@ -1,5 +1,5 @@ /*! - * mybb-april-fools 1.1.0 (21.03.2024) + * mybb-april-fools 1.1.1 (22.03.2024) * https://forumd.ru/viewtopic.php?id=7372 * Система первоапрельских розыгрышей для форумов MyBB * @author kozhilya @@ -27,6 +27,17 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ }), +/***/ "./src/html/joke-fresh_glass.html": +/*!****************************************!*\ + !*** ./src/html/joke-fresh_glass.html ***! + \****************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n// Module\nvar code = \"
\\n
\\n

А ты трогал сегодня траву?

\\n\\n \\\"Здесь\\n\\n \\n Изображение травы взято с\\n WikiMedia\\n под лицензией\\n CC BY-SA 4.0 DEED.\\n \\n
\\n
\";\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (code);\n\n//# sourceURL=webpack://mybb-april-fools/./src/html/joke-fresh_glass.html?"); + +/***/ }), + /***/ "./src/html/styles.html": /*!******************************!*\ !*** ./src/html/styles.html ***! @@ -34,7 +45,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n// Module\nvar code = \"\\r\\n\";\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (code);\n\n//# sourceURL=webpack://mybb-april-fools/./src/html/styles.html?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => (__WEBPACK_DEFAULT_EXPORT__)\n/* harmony export */ });\n// Module\nvar code = \"\\r\\n\";\n// Exports\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (code);\n\n//# sourceURL=webpack://mybb-april-fools/./src/html/styles.html?"); /***/ }), @@ -76,7 +87,7 @@ eval("\n\n//# sourceURL=webpack://mybb-april-fools/./src/types/mybb.d.ts?"); /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ debug: () => (/* binding */ debug),\n/* harmony export */ enableDebug: () => (/* binding */ enableDebug),\n/* harmony export */ getTextNodes: () => (/* binding */ getTextNodes)\n/* harmony export */ });\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! jquery */ \"jquery\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_0__);\n\n/**\n * Получение всех элементов, являющимися TextNode.\n * Используется некоторыми шутками для уменьшения\n * влияния на существующее DOM-дерево.\n *\n * @param {HTMLElement | JQuery | JQuery.Selector} root\n * Элемет, у которого необходимо найти текстовые ноды\n *\n * @return {JQuery}\n */\nfunction getTextNodes(root) {\n // Это очень странный код, но надо успокоить ESLint\n const $root = (typeof root === 'string') ? jquery__WEBPACK_IMPORTED_MODULE_0___default()(root) : jquery__WEBPACK_IMPORTED_MODULE_0___default()(root);\n return $root.\n find('*').\n contents().\n filter((_, node) => node.nodeType === Node.TEXT_NODE);\n}\nlet debugEnabled = ((scriptElement) => {\n if (!(scriptElement instanceof HTMLScriptElement)) {\n return false;\n }\n return !/\\.min\\.js$/.test(scriptElement.src);\n})(document.currentScript);\n/**\n * Отоброзить отладочное сообщение\n */\nfunction debug(...data) {\n if (!debugEnabled)\n return;\n console.log(...data);\n}\n/**\n * Включить отладку\n */\nfunction enableDebug() {\n debugEnabled = true;\n}\n\n\n//# sourceURL=webpack://mybb-april-fools/./src/common.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ d2: () => (/* binding */ d2),\n/* harmony export */ debug: () => (/* binding */ debug),\n/* harmony export */ debugEnabled: () => (/* binding */ debugEnabled),\n/* harmony export */ enableDebug: () => (/* binding */ enableDebug),\n/* harmony export */ getTextNodes: () => (/* binding */ getTextNodes),\n/* harmony export */ isFloat: () => (/* binding */ isFloat),\n/* harmony export */ isInt: () => (/* binding */ isInt),\n/* harmony export */ isNumber: () => (/* binding */ isNumber),\n/* harmony export */ isTouchEnabled: () => (/* binding */ isTouchEnabled)\n/* harmony export */ });\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! jquery */ \"jquery\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_0__);\n\n/**\n * Получение всех элементов, являющимися TextNode.\n * Используется некоторыми шутками для уменьшения\n * влияния на существующее DOM-дерево.\n *\n * @param {HTMLElement | JQuery | JQuery.Selector} root\n * Элемет, у которого необходимо найти текстовые ноды\n *\n * @return {JQuery}\n */\nfunction getTextNodes(root) {\n // Это очень странный код, но надо успокоить ESLint\n const $root = (typeof root === 'string') ? jquery__WEBPACK_IMPORTED_MODULE_0___default()(root) : jquery__WEBPACK_IMPORTED_MODULE_0___default()(root);\n return $root.\n find('*').\n contents().\n filter((_, node) => node.nodeType === Node.TEXT_NODE);\n}\nlet debugEnabled = ((scriptElement) => {\n if (!(scriptElement instanceof HTMLScriptElement)) {\n return false;\n }\n return !/\\.min\\.js$/.test(scriptElement.src);\n})(document.currentScript);\n/**\n * Отоброзить отладочное сообщение\n */\nfunction debug(...data) {\n if (!debugEnabled)\n return;\n console.log(...data);\n}\n/**\n * Включить отладку\n */\nfunction enableDebug() {\n debugEnabled = true;\n}\n/**\n * Проверка сенсорного экрана\n * @return {boolean}\n */\nfunction isTouchEnabled() {\n var _a;\n return 'ontouchstart' in window ||\n navigator.maxTouchPoints > 0 ||\n // eslint-disable-next-line\n ((_a = navigator.msMaxTouchPoints) !== null && _a !== void 0 ? _a : 0) > 0;\n}\n/**\n * Проверка, является ли переменная числом\n * @param {any} n Проверяемая переменная\n * @return {boolean} Результат проверки\n */\n// eslint-disable-next-line\nfunction isNumber(n) {\n return Number(n) === n;\n}\n/**\n * Проверка, является ли переменная целым числом\n * @param {any} n Проверяемая переменная\n * @return {boolean} Результат проверки\n */\n// eslint-disable-next-line\nfunction isInt(n) {\n return Number(n) === n && n % 1 === 0;\n}\n/**\n * Проверка, является ли переменная вещественным числом\n * @param {any} n Проверяемая переменная\n * @return {boolean} Результат проверки\n */\n// eslint-disable-next-line\nfunction isFloat(n) {\n return Number(n) === n && n % 1 !== 0;\n}\n/**\n * Тег литеральной функции, в которой вещественные числа будут\n * печататься с 2 точками после запятой\n * @param {TemplateStringsArray} strings\n * @param {any[]} values\n * @return {string}\n */\n// eslint-disable-next-line\nfunction d2(strings, ...values) {\n let result = '';\n for (let i = 0; i < strings.length; i++) {\n result += strings[i];\n if (i < values.length) {\n result += isFloat(values[i]) ? values[i].toFixed(2) : values[i];\n }\n }\n return result;\n}\n\n\n//# sourceURL=webpack://mybb-april-fools/./src/common.ts?"); /***/ }), @@ -146,6 +157,17 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ }), +/***/ "./src/jokes/fresh_grass.ts": +/*!**********************************!*\ + !*** ./src/jokes/fresh_grass.ts ***! + \**********************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ FreshGrassJoke: () => (/* binding */ FreshGrassJoke),\n/* harmony export */ FreshGrassJokeSettings: () => (/* binding */ FreshGrassJokeSettings)\n/* harmony export */ });\n/* harmony import */ var _types_html_d__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../types/html.d */ \"./src/types/html.d.ts\");\n/* harmony import */ var _types_html_d__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_types_html_d__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _joke__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../joke */ \"./src/joke.ts\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! jquery */ \"jquery\");\n/* harmony import */ var jquery__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(jquery__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var _html_joke_fresh_glass_html__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ../html/joke-fresh_glass.html */ \"./src/html/joke-fresh_glass.html\");\n/* harmony import */ var _lib_vector__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ../lib/vector */ \"./src/lib/vector.ts\");\n/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ../common */ \"./src/common.ts\");\n\n\n\n\n\n\n/**\n * **Свежая трава**\n *\n * Предложение потрогать траву\n * @author Идея: Magia\n * @author Kozhilya\n */\nclass FreshGrassJoke extends _joke__WEBPACK_IMPORTED_MODULE_1__.Joke {\n constructor() {\n super(...arguments);\n this.id = 'fresh_grass';\n this.title = 'Свежая трава';\n this.description = 'Предложение потрогать траву';\n this._settings = new FreshGrassJokeSettings();\n }\n /**\n * Запуск шутки\n */\n start() {\n jquery__WEBPACK_IMPORTED_MODULE_2___default()(() => this.init());\n }\n /**\n * Основная загрузка шутки\n */\n init() {\n const $body = jquery__WEBPACK_IMPORTED_MODULE_2___default()(document.body);\n document.body.style.setProperty('--fresh-grass-body-top', `-${window.scrollY}px`);\n this.container = jquery__WEBPACK_IMPORTED_MODULE_2___default()(_html_joke_fresh_glass_html__WEBPACK_IMPORTED_MODULE_3__[\"default\"]);\n this.container.hide(0);\n $body.append(this.container);\n this.container.fadeIn(500);\n this.fleeingGrass();\n }\n /**\n * Отладка\n * @param {string} str\n */\n debug(str) {\n if (!_common__WEBPACK_IMPORTED_MODULE_5__.debugEnabled)\n return;\n jquery__WEBPACK_IMPORTED_MODULE_2___default()('small', this.container).html(`
${str}
`);\n }\n /**\n * Анимация убегания травы от пользователя\n */\n fleeingGrass() {\n this.movement = new Movement(this.settings);\n const $img = jquery__WEBPACK_IMPORTED_MODULE_2___default()('img.april-fools__joke__fresh-grass__image', this.container);\n $img.on('mousedown', () => {\n this.close();\n });\n jquery__WEBPACK_IMPORTED_MODULE_2___default()(window).on('mousemove', (e) => {\n this.movement.mouse = new _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector(e.clientX - window.innerWidth / 2, e.clientY - window.innerHeight / 2);\n });\n this.intervalIndex = setInterval(() => {\n const img = $img[0].getBoundingClientRect();\n this.movement.step(img);\n // this.debug(this.movement.debugLines.join('\\n'));\n $img.css('transform', this.movement.state.position.toTransform());\n }, Math.floor(1000 * this.movement.dt));\n }\n /**\n * Закрыть окно\n */\n close() {\n clearInterval(this.intervalIndex);\n this.container.fadeOut({\n duration: 500,\n // eslint-disable-next-line\n // @ts-ignore\n complete: () => {\n this.container.remove();\n scrollTo(0, -parseInt(document.body.style.getPropertyValue('--fresh-grass-body-top')));\n },\n });\n }\n}\n/**\n * Класс обработки движения\n */\nclass Movement {\n /**\n * Конструктор\n * @param {FreshGrassJokeSettings} settings\n */\n constructor(settings) {\n this.settings = settings;\n this.state = {\n position: new _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector(),\n velocity: _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector.fromLengthAngle(Math.random() * 2 * Math.PI, Math.random() *\n (this.settings.maxVelocity - this.settings.minVelocity) +\n this.settings.minVelocity),\n acceleration: new _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector(),\n };\n this.mouse = new _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector();\n this.dt = 1.0 / this.settings.tickPerSecond;\n }\n /**\n * Координата движения\n */\n get position() {\n return this.state.position;\n }\n /**\n * Скорость движения\n */\n get velocity() {\n return this.state.velocity;\n }\n /**\n * Ускорение движения\n */\n get acceleration() {\n return this.state.acceleration;\n }\n /**\n * Произвести вычисление\n * @param {DOMRect} img Изображение\n */\n step(img) {\n this.last = this.state;\n this.debugLines = [];\n this.forces = {};\n // Обработка сил\n this.processForces();\n this.processMovement();\n this.processCollisions(img);\n }\n /**\n * Обработка сил\n */\n processForces() {\n // Притяжение к центру\n this.forces.gravity = _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector.fromLengthAngle(this.last.position.angle + Math.PI, this.settings.gravity);\n // Коррекция скорости\n if (this.last.velocity.length < this.settings.minVelocity) {\n this.forces.correction = _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector.fromLengthAngle(this.last.velocity.angle, this.settings.correctionForce *\n Math.pow(this.last.velocity.length - this.settings.minVelocity, 2));\n }\n else if (this.last.velocity.length > this.settings.maxVelocity) {\n this.forces.correction = _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector.fromLengthAngle(this.last.velocity.angle + Math.PI, this.settings.correctionForce *\n Math.pow(this.last.velocity.length - this.settings.maxVelocity, 2));\n }\n // Отталкивание от мыши\n const toMouseVector = this.mouse.add(this.last.position.reverse());\n if (toMouseVector.length < this.settings.keepDistance &&\n toMouseVector.length > 0) {\n this.forces.running = _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector.fromLengthAngle(toMouseVector.angle + Math.PI, this.settings.mouseForceMax -\n Math.pow(Math.abs(toMouseVector.length / this.settings.keepDistance - 1), this.settings.mouseForcePow));\n }\n }\n /**\n * Обработка движения\n */\n processMovement() {\n // Ускорение\n this.debugLines.push('Forces:');\n let acceleration = new _lib_vector__WEBPACK_IMPORTED_MODULE_4__.Vector();\n for (const [name, force] of Object.entries(this.forces)) {\n acceleration = acceleration.add(force);\n this.debugLines.push(` [${name}] ${force}`);\n }\n acceleration = acceleration.multiply(1 / this.settings.mass);\n this.state = {\n position: this.last.position,\n velocity: this.last.velocity,\n acceleration: acceleration,\n };\n this.state.velocity = this.velocity.add(this.acceleration.multiply(this.dt));\n this.state.position = this.position.add(this.velocity.multiply(this.dt));\n this.debugLines.push(`acceleration: ${this.acceleration}`);\n this.debugLines.push(`velocity: ${this.velocity}`);\n this.debugLines.push(`position: ${this.position}`);\n }\n /**\n * Обработка столкновения с границами окна\n * @param {DOMRect} img Изображение\n */\n processCollisions(img) {\n const limitX = (window.innerWidth - img.width) / 2;\n const limitY = (window.innerHeight - img.height) / 2;\n if (this.position.x < -limitX) {\n // Left\n this.position.x = -2 * limitX - this.position.x;\n this.velocity.x = -this.velocity.x * this.settings.collisionElasticity;\n }\n if (this.position.x > limitX) {\n // Right\n this.position.x = 2 * limitX - this.position.x;\n this.velocity.x = -this.velocity.x * this.settings.collisionElasticity;\n }\n if (this.position.y < -limitY) {\n // Top\n this.position.y = -2 * limitY - this.position.y;\n this.velocity.y = -this.velocity.y * this.settings.collisionElasticity;\n }\n if (this.position.y > limitY) {\n // Bottom\n this.position.y = 2 * limitY - this.position.y;\n this.velocity.y = -this.velocity.y * this.settings.collisionElasticity;\n }\n }\n /**\n * Нормально распределённая случайная величина\n * @param {number} mean\n * @param {number} stdev\n * @return {number}\n * @private\n */\n randomGaussian(mean = 0, stdev = 1) {\n const u = 1 - Math.random(); // Converting [0,1) to (0,1]\n const v = Math.random();\n const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);\n // Transform to the desired mean and standard deviation:\n return z * stdev + mean;\n }\n}\n/**\n * Класс настроек для шутки \"Свежая трава\"\n */\nclass FreshGrassJokeSettings {\n constructor() {\n this.enabled = true;\n this.chance = 100;\n /**\n * Расстояние, на котором трава начнёт убегать\n */\n this.keepDistance = 200;\n /**\n * Максимальная скорость картинки (в пикселях в секунду)\n */\n this.maxVelocity = 500;\n /**\n * Минимальная скорость картинки (в пикселях в секунду)\n */\n this.minVelocity = 20;\n /**\n * Количество отметок времени за секунду\n */\n this.tickPerSecond = 50;\n this.gravity = 20;\n this.correctionForce = 0.8;\n this.mouseForceMax = 5000;\n this.mouseForcePow = 5;\n this.mass = 1;\n this.collisionElasticity = 0.9;\n }\n}\n\n\n//# sourceURL=webpack://mybb-april-fools/./src/jokes/fresh_grass.ts?"); + +/***/ }), + /***/ "./src/jokes/humanitarian_hell.ts": /*!****************************************!*\ !*** ./src/jokes/humanitarian_hell.ts ***! @@ -223,6 +245,17 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ }), +/***/ "./src/lib/vector.ts": +/*!***************************!*\ + !*** ./src/lib/vector.ts ***! + \***************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ Vector: () => (/* binding */ Vector)\n/* harmony export */ });\n/* harmony import */ var _common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../common */ \"./src/common.ts\");\n\n/**\n * Класс вектора\n */\nclass Vector {\n /**\n * Конструктор вектора\n * @param {number} x Координата X\n * @param {number} y Координата Y\n */\n constructor(x = 0, y = 0) {\n this.x = x;\n this.y = y;\n }\n /**\n * Угол наклона вектора\n */\n get angle() {\n return Math.atan2(this.y, this.x);\n }\n /**\n * Угол наклона вектора\n * @param {number} angle\n */\n set angle(angle) {\n const length = this.length;\n this.x = Math.cos(angle) * length;\n this.y = Math.sin(angle) * length;\n }\n /**\n * Длина вектора\n */\n get length() {\n return Math.distance(this.x, this.y);\n }\n /**\n * Длина вектора\n * @param {number} length\n */\n set length(length) {\n const angle = this.angle;\n this.x = Math.cos(angle) * length;\n this.y = Math.sin(angle) * length;\n }\n /**\n * Сумма векторов\n * @param {Vector} a Первый вектор\n * @param {Vector} b Второй вектор\n * @return {Vector} Вектор суммы\n */\n static addVectors(a, b) {\n return new Vector(a.x + b.x, a.y + b.y);\n }\n /**\n * Скалярное умножение векторов\n * @param {Vector} a Первый вектор\n * @param {Vector} b Второй вектор\n * @return {number} Скалярное произведение векторов\n */\n static dotProduct(a, b) {\n return a.x * b.x + a.y * b.y;\n }\n /**\n * Создать вектор по углу наклона и длине\n * @param {number} angle Угол наклона\n * @param {number} length Длина\n * @return {Vector} Вектор суммы\n */\n static fromLengthAngle(angle, length) {\n const v = new Vector();\n v.length = length;\n v.angle = angle;\n return v;\n }\n /**\n * Нахождение противоположного вектора\n * @return {Vector} Противоположный вектор\n */\n reverse() {\n return new Vector(-this.x, -this.y);\n }\n /**\n * Прибавить вектор\n * @param {Vector} a Второе слагаемое\n * @return {Vector} Вектор суммы\n */\n add(a) {\n return Vector.addVectors(this, a);\n }\n /**\n * Умножение вектора на число\n * @param {number} a Скаляр\n * @return {Vector} Вектор-результат\n */\n multiply(a) {\n return new Vector(a * this.x, a * this.y);\n }\n /**\n * Преобразование в CSS-преобразование\n * @return {string} CSS-преобразование\n */\n toTransform() {\n return `translate(${this.x}px, ${this.y}px)`;\n }\n /**\n * Строковое представление\n * @return {string}\n */\n toString() {\n return (0,_common__WEBPACK_IMPORTED_MODULE_0__.d2) `Vector(x=${this.x}, y=${this.y}, ` +\n (0,_common__WEBPACK_IMPORTED_MODULE_0__.d2) `length=${this.length}, angle=${this.angle})`;\n }\n}\nMath.distance = (x, y) => Math.sqrt(x * x + y * y);\n\n\n//# sourceURL=webpack://mybb-april-fools/./src/lib/vector.ts?"); + +/***/ }), + /***/ "./src/main.ts": /*!*********************!*\ !*** ./src/main.ts ***! @@ -230,7 +263,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _joker__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./joker */ \"./src/joker.ts\");\n/* harmony import */ var _jokes_shake__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./jokes/shake */ \"./src/jokes/shake.ts\");\n/* harmony import */ var _jokes_humanitarian_hell__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./jokes/humanitarian_hell */ \"./src/jokes/humanitarian_hell.ts\");\n/* harmony import */ var _jokes_mirror__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./jokes/mirror */ \"./src/jokes/mirror.ts\");\n/* harmony import */ var _jokes_carnival__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./jokes/carnival */ \"./src/jokes/carnival.ts\");\n/* harmony import */ var _jokes_crazy_letters__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./jokes/crazy_letters */ \"./src/jokes/crazy_letters.ts\");\n/* harmony import */ var _jokes_letter_chaos__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./jokes/letter_chaos */ \"./src/jokes/letter_chaos.ts\");\n/* harmony import */ var _jokes_cursor__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./jokes/cursor */ \"./src/jokes/cursor.ts\");\n/* harmony import */ var _jokes_stupid_title__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./jokes/stupid_title */ \"./src/jokes/stupid_title.ts\");\n/* harmony import */ var _jokes_inverted_scroll__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./jokes/inverted_scroll */ \"./src/jokes/inverted_scroll.ts\");\n/* harmony import */ var _jokes_latinization__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./jokes/latinization */ \"./src/jokes/latinization.ts\");\n\n\n\n\n\n\n\n\n\n\n\nwindow.AprilJokes = ((jokes) => {\n const cl = new _joker__WEBPACK_IMPORTED_MODULE_0__.JokerClass();\n jokes.forEach((CustomJoke) => {\n cl.add(new CustomJoke());\n });\n $(() => cl.regularStart());\n return cl;\n})([\n _jokes_shake__WEBPACK_IMPORTED_MODULE_1__.ShakeJoke,\n _jokes_humanitarian_hell__WEBPACK_IMPORTED_MODULE_2__.HumanitarianHellJoke,\n _jokes_mirror__WEBPACK_IMPORTED_MODULE_3__.MirrorJoke,\n _jokes_carnival__WEBPACK_IMPORTED_MODULE_4__.CarnivalJoke,\n _jokes_crazy_letters__WEBPACK_IMPORTED_MODULE_5__.CrazyLettersJoke,\n _jokes_letter_chaos__WEBPACK_IMPORTED_MODULE_6__.LetterChaosJoke,\n _jokes_cursor__WEBPACK_IMPORTED_MODULE_7__.CursorEffectsJoke,\n _jokes_stupid_title__WEBPACK_IMPORTED_MODULE_8__.StupidTitleJoke,\n _jokes_inverted_scroll__WEBPACK_IMPORTED_MODULE_9__.InvertedScrollJoke,\n _jokes_latinization__WEBPACK_IMPORTED_MODULE_10__.LatinizationJoke,\n]);\n\n\n//# sourceURL=webpack://mybb-april-fools/./src/main.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _joker__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./joker */ \"./src/joker.ts\");\n/* harmony import */ var _jokes_shake__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./jokes/shake */ \"./src/jokes/shake.ts\");\n/* harmony import */ var _jokes_humanitarian_hell__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./jokes/humanitarian_hell */ \"./src/jokes/humanitarian_hell.ts\");\n/* harmony import */ var _jokes_mirror__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./jokes/mirror */ \"./src/jokes/mirror.ts\");\n/* harmony import */ var _jokes_carnival__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./jokes/carnival */ \"./src/jokes/carnival.ts\");\n/* harmony import */ var _jokes_crazy_letters__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./jokes/crazy_letters */ \"./src/jokes/crazy_letters.ts\");\n/* harmony import */ var _jokes_letter_chaos__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./jokes/letter_chaos */ \"./src/jokes/letter_chaos.ts\");\n/* harmony import */ var _jokes_cursor__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./jokes/cursor */ \"./src/jokes/cursor.ts\");\n/* harmony import */ var _jokes_stupid_title__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./jokes/stupid_title */ \"./src/jokes/stupid_title.ts\");\n/* harmony import */ var _jokes_inverted_scroll__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./jokes/inverted_scroll */ \"./src/jokes/inverted_scroll.ts\");\n/* harmony import */ var _jokes_latinization__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./jokes/latinization */ \"./src/jokes/latinization.ts\");\n/* harmony import */ var _jokes_fresh_grass__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./jokes/fresh_grass */ \"./src/jokes/fresh_grass.ts\");\n\n\n\n\n\n\n\n\n\n\n\n\nwindow.AprilJokes = ((jokes) => {\n const cl = new _joker__WEBPACK_IMPORTED_MODULE_0__.JokerClass();\n jokes.forEach((CustomJoke) => {\n cl.add(new CustomJoke());\n });\n $(() => cl.regularStart());\n return cl;\n})([\n _jokes_shake__WEBPACK_IMPORTED_MODULE_1__.ShakeJoke,\n _jokes_humanitarian_hell__WEBPACK_IMPORTED_MODULE_2__.HumanitarianHellJoke,\n _jokes_mirror__WEBPACK_IMPORTED_MODULE_3__.MirrorJoke,\n _jokes_carnival__WEBPACK_IMPORTED_MODULE_4__.CarnivalJoke,\n _jokes_crazy_letters__WEBPACK_IMPORTED_MODULE_5__.CrazyLettersJoke,\n _jokes_letter_chaos__WEBPACK_IMPORTED_MODULE_6__.LetterChaosJoke,\n _jokes_cursor__WEBPACK_IMPORTED_MODULE_7__.CursorEffectsJoke,\n _jokes_stupid_title__WEBPACK_IMPORTED_MODULE_8__.StupidTitleJoke,\n _jokes_inverted_scroll__WEBPACK_IMPORTED_MODULE_9__.InvertedScrollJoke,\n _jokes_latinization__WEBPACK_IMPORTED_MODULE_10__.LatinizationJoke,\n _jokes_fresh_grass__WEBPACK_IMPORTED_MODULE_11__.FreshGrassJoke,\n]);\n\n\n//# sourceURL=webpack://mybb-april-fools/./src/main.ts?"); /***/ }), diff --git a/settings.compact.js b/settings.compact.js index e5aee0b..b0c04e5 100644 --- a/settings.compact.js +++ b/settings.compact.js @@ -190,4 +190,21 @@ window.AprilJokes.settings = { 'y': 'ы', 'z': 'з', }, }, + + fresh_grass: { + enabled: true, + + chance: 100, + + keepDistance: 200, + maxVelocity: 500, + minVelocity: 20, + tickPerSecond: 50, + gravity: 20, + correctionForce: 0.8, + mouseForceMax: 5000, + mouseForcePow: 5, + mass: 1, + collisionElasticity: 0.9, + } }; diff --git a/settings.example.js b/settings.example.js index 299a922..e143f3e 100644 --- a/settings.example.js +++ b/settings.example.js @@ -507,4 +507,67 @@ window.AprilJokes.settings = { 'y': 'ы', 'z': 'з', }, }, + + /** + * **Свежая трава** + * + * Предложение потрогать траву + * @author Идея: Magia + * @author Kozhilya + */ + fresh_grass: { + enabled: true, + + chance: 100, + + /** + * Расстояние, на котором трава начнёт убегать + */ + keepDistance: 200, + + /** + * Максимальная скорость картинки (в пикселях в секунду) + */ + maxVelocity: 500, + + /** + * Минимальная скорость картинки (в пикселях в секунду) + */ + minVelocity: 20, + + /** + * Количество отметок времени за секунду + */ + tickPerSecond: 50, + + /** + * Сила притяжения в центр + */ + gravity: 20, + + /** + * Сила исправления скорости + */ + correctionForce: 0.8, + + /** + * Максимальная сила отталкивания от курсора + */ + mouseForceMax: 5000, + + /** + * Степень увеличения силы отталкивания от курсора + */ + mouseForcePow: 5, + + /** + * Общая масса картинки + */ + mass: 1, + + /** + * Доля скорости, сохраняемой при столкновении со стеной + */ + collisionElasticity: 0.9, + } }; diff --git a/src/common.ts b/src/common.ts index a14f652..cd5d558 100644 --- a/src/common.ts +++ b/src/common.ts @@ -23,7 +23,7 @@ export function getTextNodes( filter((_, node) => node.nodeType === Node.TEXT_NODE); } -let debugEnabled = ((scriptElement) => { +export let debugEnabled = ((scriptElement) => { if (!(scriptElement instanceof HTMLScriptElement)) { return false; } @@ -33,7 +33,7 @@ let debugEnabled = ((scriptElement) => { /** * Отоброзить отладочное сообщение */ -export function debug(...data: (string|number|object)[]) { +export function debug(...data: (string | number | object)[]) { if (!debugEnabled) return; console.log(...data); } @@ -44,3 +44,63 @@ export function debug(...data: (string|number|object)[]) { export function enableDebug() { debugEnabled = true; } + +/** + * Проверка сенсорного экрана + * @return {boolean} + */ +export function isTouchEnabled(): boolean { + return 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + // eslint-disable-next-line + ((navigator as any).msMaxTouchPoints ?? 0) > 0; +} + +/** + * Проверка, является ли переменная числом + * @param {any} n Проверяемая переменная + * @return {boolean} Результат проверки + */ +// eslint-disable-next-line +export function isNumber(n: any): boolean { + return Number(n) === n; +} + +/** + * Проверка, является ли переменная целым числом + * @param {any} n Проверяемая переменная + * @return {boolean} Результат проверки + */ +// eslint-disable-next-line +export function isInt(n: any): boolean { + return Number(n) === n && n % 1 === 0; +} + +/** + * Проверка, является ли переменная вещественным числом + * @param {any} n Проверяемая переменная + * @return {boolean} Результат проверки + */ +// eslint-disable-next-line +export function isFloat(n: any): boolean { + return Number(n) === n && n % 1 !== 0; +} + +/** + * Тег литеральной функции, в которой вещественные числа будут + * печататься с 2 точками после запятой + * @param {TemplateStringsArray} strings + * @param {any[]} values + * @return {string} + */ +// eslint-disable-next-line +export function d2(strings: TemplateStringsArray, ...values: any[]): string { + let result = ''; + for (let i = 0; i < strings.length; i++) { + result += strings[i]; + if (i < values.length) { + result += isFloat(values[i]) ? values[i].toFixed(2) : values[i]; + } + } + return result; +} diff --git a/src/html/joke-fresh_glass.html b/src/html/joke-fresh_glass.html new file mode 100644 index 0000000..13c8e1c --- /dev/null +++ b/src/html/joke-fresh_glass.html @@ -0,0 +1,16 @@ +
+
+

А ты трогал сегодня траву?

+ + Здесь должная быть трава, но что-то сломалось... Притворись, что она здесь есть, хорошо? + + + Изображение травы взято с + WikiMedia + под лицензией + CC BY-SA 4.0 DEED. + +
+
\ No newline at end of file diff --git a/src/html/styles.html b/src/html/styles.html index 52b128a..95c2f86 100644 --- a/src/html/styles.html +++ b/src/html/styles.html @@ -1,65 +1,129 @@ diff --git a/src/jokes/fresh_grass.ts b/src/jokes/fresh_grass.ts new file mode 100644 index 0000000..a751943 --- /dev/null +++ b/src/jokes/fresh_grass.ts @@ -0,0 +1,362 @@ +import '../types/html.d'; +import {Joke, JokeSettings} from '../joke'; +import $ from 'jquery'; +import containerHTML from '../html/joke-fresh_glass.html'; +import {Vector} from '../lib/vector'; +import {debugEnabled} from '../common'; + +/** + * **Свежая трава** + * + * Предложение потрогать траву + * @author Идея: Magia + * @author Kozhilya + */ +export class FreshGrassJoke extends Joke { + id = 'fresh_grass'; + + title = 'Свежая трава'; + + description = 'Предложение потрогать траву'; + + _settings = new FreshGrassJokeSettings(); + + container: JQuery; + + intervalIndex: NodeJS.Timeout; + + movement: Movement; + + /** + * Запуск шутки + */ + start(): void { + $(() => this.init()); + } + + /** + * Основная загрузка шутки + */ + init() { + const $body = $(document.body); + + document.body.style.setProperty( + '--fresh-grass-body-top', + `-${window.scrollY}px`, + ); + + this.container = $(containerHTML); + this.container.hide(0); + $body.append(this.container); + this.container.fadeIn(500); + + this.fleeingGrass(); + } + + /** + * Отладка + * @param {string} str + */ + debug(str: string): void { + if (!debugEnabled) return; + + $('small', this.container).html(`
${str}
`); + } + + /** + * Анимация убегания травы от пользователя + */ + fleeingGrass(): void { + this.movement = new Movement(this.settings); + + const $img = $('img.april-fools__joke__fresh-grass__image', this.container); + + $img.on('mousedown', () => { + this.close(); + }); + + $(window).on('mousemove', (e) => { + this.movement.mouse = new Vector(e.clientX - window.innerWidth / 2, + e.clientY - window.innerHeight / 2); + }); + + this.intervalIndex = setInterval(() => { + const img = $img[0].getBoundingClientRect(); + this.movement.step(img); + + // this.debug(this.movement.debugLines.join('\n')); + + $img.css('transform', this.movement.state.position.toTransform()); + }, Math.floor(1000 * this.movement.dt)); + } + + /** + * Закрыть окно + */ + close(): void { + clearInterval(this.intervalIndex); + + this.container.fadeOut({ + duration: 500, + // eslint-disable-next-line + // @ts-ignore + complete: () => { + this.container.remove(); + + scrollTo(0, + -parseInt(document.body.style.getPropertyValue( + '--fresh-grass-body-top'))); + }, + }); + } +} + +type MovementData = { + position: Vector; + velocity: Vector; + acceleration: Vector; +}; + +/** + * Класс обработки движения + */ +class Movement { + settings: FreshGrassJokeSettings; + state: MovementData; + mouse: Vector; + dt: number; + last: MovementData; + debugLines: string[]; + forces: { [key in string]: Vector }; + + /** + * Конструктор + * @param {FreshGrassJokeSettings} settings + */ + constructor(settings: FreshGrassJokeSettings) { + this.settings = settings; + this.state = { + position: new Vector(), + velocity: Vector.fromLengthAngle(Math.random() * 2 * Math.PI, + Math.random() * + (this.settings.maxVelocity - this.settings.minVelocity) + + this.settings.minVelocity), + acceleration: new Vector(), + }; + this.mouse = new Vector(); + this.dt = 1.0 / this.settings.tickPerSecond; + } + + /** + * Координата движения + */ + get position() { + return this.state.position; + } + + /** + * Скорость движения + */ + get velocity() { + return this.state.velocity; + } + + /** + * Ускорение движения + */ + get acceleration() { + return this.state.acceleration; + } + + /** + * Произвести вычисление + * @param {DOMRect} img Изображение + */ + step(img: DOMRect) { + this.last = this.state; + this.debugLines = []; + this.forces = {}; + + // Обработка сил + this.processForces(); + this.processMovement(); + this.processCollisions(img); + } + + /** + * Обработка сил + */ + processForces() { + // Притяжение к центру + this.forces.gravity = Vector.fromLengthAngle( + this.last.position.angle + Math.PI, + this.settings.gravity, + ); + + // Коррекция скорости + if (this.last.velocity.length < this.settings.minVelocity) { + this.forces.correction = Vector.fromLengthAngle( + this.last.velocity.angle, + this.settings.correctionForce * + Math.pow(this.last.velocity.length - this.settings.minVelocity, 2), + ); + } else if (this.last.velocity.length > this.settings.maxVelocity) { + this.forces.correction = Vector.fromLengthAngle( + this.last.velocity.angle + Math.PI, + this.settings.correctionForce * + Math.pow(this.last.velocity.length - this.settings.maxVelocity, 2), + ); + } + + // Отталкивание от мыши + const toMouseVector = this.mouse.add(this.last.position.reverse()); + if (toMouseVector.length < this.settings.keepDistance && + toMouseVector.length > 0) { + this.forces.running = Vector.fromLengthAngle( + toMouseVector.angle + Math.PI, + this.settings.mouseForceMax - + Math.pow( + Math.abs(toMouseVector.length / this.settings.keepDistance - 1), + this.settings.mouseForcePow), + ); + } + } + + /** + * Обработка движения + */ + processMovement() { + // Ускорение + this.debugLines.push('Forces:'); + let acceleration: Vector = new Vector(); + + for (const [name, force] of Object.entries(this.forces)) { + acceleration = acceleration.add(force); + this.debugLines.push(` [${name}] ${force}`); + } + + acceleration = acceleration.multiply(1 / this.settings.mass); + + this.state = { + position: this.last.position, + velocity: this.last.velocity, + acceleration: acceleration, + }; + + this.state.velocity = this.velocity.add( + this.acceleration.multiply(this.dt)); + this.state.position = this.position.add( + this.velocity.multiply(this.dt)); + + this.debugLines.push(`acceleration: ${this.acceleration}`); + this.debugLines.push(`velocity: ${this.velocity}`); + this.debugLines.push(`position: ${this.position}`); + } + + /** + * Обработка столкновения с границами окна + * @param {DOMRect} img Изображение + */ + processCollisions(img: DOMRect) { + const limitX = (window.innerWidth - img.width) / 2; + const limitY = (window.innerHeight - img.height) / 2; + + if (this.position.x < -limitX) { + // Left + this.position.x = -2 * limitX - this.position.x; + this.velocity.x = -this.velocity.x * this.settings.collisionElasticity; + } + + if (this.position.x > limitX) { + // Right + this.position.x = 2 * limitX - this.position.x; + this.velocity.x = -this.velocity.x * this.settings.collisionElasticity; + } + + if (this.position.y < -limitY) { + // Top + this.position.y = -2 * limitY - this.position.y; + this.velocity.y = -this.velocity.y * this.settings.collisionElasticity; + } + + if (this.position.y > limitY) { + // Bottom + this.position.y = 2 * limitY - this.position.y; + this.velocity.y = -this.velocity.y * this.settings.collisionElasticity; + } + } + + /** + * Нормально распределённая случайная величина + * @param {number} mean + * @param {number} stdev + * @return {number} + * @private + */ + private randomGaussian(mean: number = 0, stdev: number = 1): number { + const u = 1 - Math.random(); // Converting [0,1) to (0,1] + const v = Math.random(); + const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v); + // Transform to the desired mean and standard deviation: + return z * stdev + mean; + } +} + +/** + * Класс настроек для шутки "Свежая трава" + */ +export class FreshGrassJokeSettings implements JokeSettings { + enabled: boolean = true; + + chance: number = 100; + + /** + * Расстояние, на котором трава начнёт убегать + */ + keepDistance: number = 200; + + /** + * Максимальная скорость картинки (в пикселях в секунду) + */ + maxVelocity: number = 500; + + /** + * Минимальная скорость картинки (в пикселях в секунду) + */ + minVelocity: number = 20; + + /** + * Количество отметок времени за секунду + */ + tickPerSecond: number = 50; + + /** + * Сила притяжения в центр + */ + gravity: number = 20; + + /** + * Сила исправления скорости + */ + correctionForce: number = 0.8; + + /** + * Максимальная сила отталкивания от курсора + */ + mouseForceMax: number = 5000; + + /** + * Степень увеличения силы отталкивания от курсора + */ + mouseForcePow: number = 5; + + /** + * Общая масса картинки + */ + mass: number = 1; + + /** + * Доля скорости, сохраняемой при столкновении со стеной + */ + collisionElasticity: number = 0.9; +} diff --git a/src/lib/vector.ts b/src/lib/vector.ts new file mode 100644 index 0000000..5c0a1b7 --- /dev/null +++ b/src/lib/vector.ts @@ -0,0 +1,150 @@ +import {d2} from '../common'; + +/** + * Класс вектора + */ +export class Vector { + /** + * Координата X + */ + x: number; + + /** + * Координата Y + */ + y: number; + + /** + * Конструктор вектора + * @param {number} x Координата X + * @param {number} y Координата Y + */ + constructor(x: number = 0, y: number = 0) { + this.x = x; + this.y = y; + } + + /** + * Угол наклона вектора + */ + get angle() { + return Math.atan2(this.y, this.x); + } + + /** + * Угол наклона вектора + * @param {number} angle + */ + set angle(angle: number) { + const length = this.length; + this.x = Math.cos(angle) * length; + this.y = Math.sin(angle) * length; + } + + /** + * Длина вектора + */ + get length() { + return Math.distance(this.x, this.y); + } + + /** + * Длина вектора + * @param {number} length + */ + set length(length: number) { + const angle = this.angle; + this.x = Math.cos(angle) * length; + this.y = Math.sin(angle) * length; + } + + /** + * Сумма векторов + * @param {Vector} a Первый вектор + * @param {Vector} b Второй вектор + * @return {Vector} Вектор суммы + */ + static addVectors(a: Vector, b: Vector): Vector { + return new Vector(a.x + b.x, a.y + b.y); + } + + /** + * Скалярное умножение векторов + * @param {Vector} a Первый вектор + * @param {Vector} b Второй вектор + * @return {number} Скалярное произведение векторов + */ + static dotProduct(a: Vector, b: Vector): number { + return a.x * b.x + a.y * b.y; + } + + /** + * Создать вектор по углу наклона и длине + * @param {number} angle Угол наклона + * @param {number} length Длина + * @return {Vector} Вектор суммы + */ + static fromLengthAngle(angle: number, length: number): Vector { + const v = new Vector(); + v.length = length; + v.angle = angle; + return v; + } + + /** + * Нахождение противоположного вектора + * @return {Vector} Противоположный вектор + */ + reverse(): Vector { + return new Vector(-this.x, -this.y); + } + + /** + * Прибавить вектор + * @param {Vector} a Второе слагаемое + * @return {Vector} Вектор суммы + */ + add(a: Vector): Vector { + return Vector.addVectors(this, a); + } + + /** + * Умножение вектора на число + * @param {number} a Скаляр + * @return {Vector} Вектор-результат + */ + multiply(a: number): Vector { + return new Vector(a * this.x, a * this.y); + } + + /** + * Преобразование в CSS-преобразование + * @return {string} CSS-преобразование + */ + toTransform(): string { + return `translate(${this.x}px, ${this.y}px)`; + } + + /** + * Строковое представление + * @return {string} + */ + toString(): string { + return d2`Vector(x=${this.x}, y=${this.y}, ` + + d2`length=${this.length}, angle=${this.angle})`; + } +} + +declare global { + interface Math { + /** + * Расчёт длины вектора + * @param {number} x Координата X вектора + * @param {number} y Координата X вектора + * @return {number} Длина вектора + */ + distance(x: number, y: number): number; + } +} + +Math.distance = (x, y) => Math.sqrt(x * x + y * y); diff --git a/src/main.ts b/src/main.ts index 0e6a141..4c0219c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import {CursorEffectsJoke} from './jokes/cursor'; import {StupidTitleJoke} from './jokes/stupid_title'; import {InvertedScrollJoke} from './jokes/inverted_scroll'; import {LatinizationJoke} from './jokes/latinization'; +import {FreshGrassJoke} from './jokes/fresh_grass'; declare global { interface Window { AprilJokes: JokerClass; } @@ -36,6 +37,7 @@ window.AprilJokes = ((jokes: JokeList): JokerClass => { StupidTitleJoke, InvertedScrollJoke, LatinizationJoke, + FreshGrassJoke, ]); interface JokeList extends Array Joke> {}