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 = \"
${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 @@
+${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