diff --git a/.gitattributes b/.gitattributes index e69de29..d787998 100644 --- a/.gitattributes +++ b/.gitattributes @@ -0,0 +1,2 @@ +jekyll_site/ru/** linguist-language=JavaScript +jekyll_site/en/** linguist-language=JavaScript diff --git a/.gitignore b/.gitignore index c38fa4e..42e9bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ .idea *.iml +*.zip +_site* +.repo_*.sh diff --git a/DIRECTORY-TREE.md b/DIRECTORY-TREE.md new file mode 100644 index 0000000..32a7034 --- /dev/null +++ b/DIRECTORY-TREE.md @@ -0,0 +1,74 @@ +## Дерево каталогов + +
+. +├─ jekyll_site +│ ├─ _includes +│ │ ├─ classes-point-cube-en.md +│ │ ├─ classes-point-cube-ru.md +│ │ ├─ counters_body.html +│ │ ├─ counters_head.html +│ │ ├─ volumetric-tetris-en.html +│ │ └─ volumetric-tetris-ru.html +│ ├─ css +│ │ └─ pomodoro1.css +│ ├─ en +│ │ ├─ 2023 +│ │ │ └─ 01 +│ │ │ ├─ 06 +│ │ │ │ └─ spinning-square-on-plane.md +│ │ │ ├─ 11 +│ │ │ │ └─ spinning-cube-in-space.md +│ │ │ ├─ 16 +│ │ │ │ └─ spinning-spatial-cross.md +│ │ │ └─ 22 +│ │ │ └─ volumetric-tetris.md +│ │ └─ index.md +│ ├─ img +│ │ ├─ central-projection.svg +│ │ ├─ column-vector2d.svg +│ │ ├─ column-vector3dx.svg +│ │ ├─ column-vector3dy.svg +│ │ ├─ column-vector3dz.svg +│ │ ├─ euclidean-distance.svg +│ │ ├─ linear-equation.svg +│ │ └─ oblique-projection.svg +│ ├─ js +│ │ ├─ classes-point-cube.js +│ │ ├─ spinning-cube.js +│ │ ├─ spinning-cube2.js +│ │ ├─ spinning-spatial-cross.js +│ │ ├─ spinning-spatial-cross2.js +│ │ ├─ spinning-square.js +│ │ ├─ spinning-square2.js +│ │ ├─ tetris-controller.js +│ │ ├─ tetris-figures.js +│ │ ├─ tetris-model.js +│ │ └─ tetris-view.js +│ ├─ ru +│ │ ├─ 2023 +│ │ │ └─ 01 +│ │ │ ├─ 05 +│ │ │ │ └─ spinning-square-on-plane.md +│ │ │ ├─ 10 +│ │ │ │ └─ spinning-cube-in-space.md +│ │ │ ├─ 15 +│ │ │ │ └─ spinning-spatial-cross.md +│ │ │ └─ 21 +│ │ │ └─ volumetric-tetris.md +│ │ └─ index.md +│ ├─ Gemfile_color +│ ├─ Gemfile_older +│ ├─ _config_color.yml +│ ├─ _config_older.yml +│ └─ robots.txt +├─ CONTRIBUTING.md +├─ DIRECTORY-TREE.md +├─ LICENSE.md +├─ OPEN_LICENSE.txt +├─ README.en.md +├─ README.md +├─ build.sh +├─ package.sh +└─ serve.sh +diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..834ee9e --- /dev/null +++ b/README.en.md @@ -0,0 +1,16 @@ +## Website pages + +- [Volumetric tetris](https://pomodoro1.mircloud.ru/en/2023/01/22/volumetric-tetris.html) — 22.01.2023. +- [Spinning spatial cross](https://pomodoro1.mircloud.ru/en/2023/01/16/spinning-spatial-cross.html) — 16.01.2023. +- [Spinning cube in space](https://pomodoro1.mircloud.ru/en/2023/01/11/spinning-cube-in-space.html) — 11.01.2023. +- [Spinning square on plane](https://pomodoro1.mircloud.ru/en/2023/01/06/spinning-square-on-plane.html) — 06.01.2023. + +## [Source texts](README.md) + +- Series of the static websites [«Pomodori»](https://hub.mos.ru/golovin.gg/pomodoro/blob/master/README.en.md). +- Used formats — Markdown, Liquid, YAML. +- Build tool — Jekyll with tomato design themes. +- Automation of processes — Bash scripts for command line. +- [build.sh](build.sh) — Building a site in two tomato themes and optimizing the results. +- [serve.sh](serve.sh) — Local deployment to verify the correctness of the build. +- [package.sh](package.sh) — Preparing an archive for subsequent deployment. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c1371a --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +## Страницы вёб-сайта + +- [Объёмный тетрис](https://pomodoro1.mircloud.ru/ru/2023/01/21/volumetric-tetris.html) — 21.01.2023. +- [Вращаем пространственный крест](https://pomodoro1.mircloud.ru/ru/2023/01/15/spinning-spatial-cross.html) — 15.01.2023. +- [Вращаем куб в пространстве](https://pomodoro1.mircloud.ru/ru/2023/01/10/spinning-cube-in-space.html) — 10.01.2023. +- [Вращаем квадрат на плоскости](https://pomodoro1.mircloud.ru/ru/2023/01/05/spinning-square-on-plane.html) — 05.01.2023. + +## [Исходные тексты](README.en.md) + +- Серия статических вёб-сайтов [«Помидоры»](https://hub.mos.ru/golovin.gg/pomodoro/blob/master/README.md). +- Используемые форматы — Markdown, Liquid, YAML. +- Инструмент сборки — Jekyll с помидорными темами оформления. +- Автоматизация процессов — Bash скрипты для командной строки. +- [build.sh](build.sh) — Сборка сайта в двух помидорных темах и оптимизация результатов. +- [serve.sh](serve.sh) — Локальное развёртывание для проверки корректности сборки. +- [package.sh](package.sh) — Подготовка архива для последующего развёртывания. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..bb1ba6a --- /dev/null +++ b/build.sh @@ -0,0 +1,56 @@ +#!/bin/bash +echo "Сборка сайта в двух помидорных темах и оптимизация результатов." +milliseconds=$(date '+%s%3N') +rm -rf _site +rm -rf _site_older +rm -rf _site_color +echo "Сборка старого помидора." +mkdir -p _site_older +cp -r jekyll_site/_includes _site_older +cp -r jekyll_site/ru _site_older +cp -r jekyll_site/en _site_older +cp -r jekyll_site/ru/index.md _site_older +cp -r jekyll_site/_config_older.yml _site_older/_config.yml +cp -r jekyll_site/Gemfile_older _site_older/Gemfile +cd _site_older || exit +jekyll build +cp -r _site .. +cd .. +echo "Сборка цветного помидора." +mkdir -p _site_color +cp -r jekyll_site/_includes _site_color +cp -r jekyll_site/ru _site_color +cp -r jekyll_site/en _site_color +cp -r jekyll_site/ru/index.md _site_color +cp -r jekyll_site/_config_color.yml _site_color/_config.yml +cp -r jekyll_site/Gemfile_color _site_color/Gemfile +cd _site_color || exit +jekyll build +cp -r _site ../_site/color +cd .. +echo "Копирование без сборки." +cp -r jekyll_site/css _site +cp -r jekyll_site/img _site +cp -r jekyll_site/js _site +cp -r jekyll_site/robots.txt _site +echo "Оптимизация собранного контента." +cd _site || exit +cp -r assets/* . +rm -r assets +rm -r color/assets/favicon.ico +cp -r color/assets/* . +rm -r color/assets +rm -r color/404.html +find . -type f -name '*.html' | sort -r | while read -r file; do + sed -i 's/layout-padding=""/layout-padding/g' "$file" + sed -i 's/ class="language-plaintext highlighter-rouge"//g' "$file" + sed -i 's/ class="language-java highlighter-rouge"//g' "$file" + sed -i 's/ class="language-html highlighter-rouge"//g' "$file" + sed -i 's/ class="language-js highlighter-rouge"//g' "$file" + sed -i 's/
//g' "$file"
+ sed -i 's/<\/code><\/pre><\/div><\/div>/<\/code><\/pre><\/div>/g' "$file"
+ sed -i 's/
/
/g' "$file"
+ sed -i -r 's///g' "$file"
+ sed -i -r 's/
/
/g' "$file"
+done
+echo "Время выполнения сборки: $(("$(date '+%s%3N')" - "$milliseconds")) мс."
diff --git a/jekyll_site/Gemfile_color b/jekyll_site/Gemfile_color
new file mode 100644
index 0000000..136ccb7
--- /dev/null
+++ b/jekyll_site/Gemfile_color
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+gem "jekyll"
+gem "color-tomato-theme"
diff --git a/jekyll_site/Gemfile_older b/jekyll_site/Gemfile_older
new file mode 100644
index 0000000..5e19e6f
--- /dev/null
+++ b/jekyll_site/Gemfile_older
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+gem "jekyll"
+gem "older-tomato-theme"
diff --git a/jekyll_site/_config_color.yml b/jekyll_site/_config_color.yml
new file mode 100644
index 0000000..a0e8fca
--- /dev/null
+++ b/jekyll_site/_config_color.yml
@@ -0,0 +1,20 @@
+# site parameters
+name: "Код с комментариями"
+name_translated: "Code with comments"
+url: "https://pomodoro1.mircloud.ru"
+baseurl: "/color"
+homepage_url: "https://git.org.ru/pomodoro/1"
+homepage_name: "GIT.ORG.RU"
+older_tomato_baseurl: ""
+timezone: "Europe/Moscow"
+author: "Головин Г.Г."
+author_translated: "Golovin G.G."
+translation_caption: "translation from Russian"
+# build parameters
+disable_disk_cache: true
+theme: color-tomato-theme
+defaults:
+ - scope:
+ path: ""
+ values:
+ layout: default
diff --git a/jekyll_site/_config_older.yml b/jekyll_site/_config_older.yml
new file mode 100644
index 0000000..2072ba5
--- /dev/null
+++ b/jekyll_site/_config_older.yml
@@ -0,0 +1,20 @@
+# site parameters
+name: "Код с комментариями"
+name_translated: "Code with comments"
+url: "https://pomodoro1.mircloud.ru"
+baseurl: ""
+homepage_url: "https://git.org.ru/pomodoro/1"
+homepage_name: "GIT.ORG.RU"
+color_tomato_baseurl: "/color"
+timezone: "Europe/Moscow"
+author: "Головин Г.Г."
+author_translated: "Golovin G.G."
+translation_caption: "translation from Russian"
+# build parameters
+disable_disk_cache: true
+theme: older-tomato-theme
+defaults:
+ - scope:
+ path: ""
+ values:
+ layout: default
diff --git a/jekyll_site/_includes/classes-point-cube-en.md b/jekyll_site/_includes/classes-point-cube-en.md
new file mode 100644
index 0000000..921a951
--- /dev/null
+++ b/jekyll_site/_includes/classes-point-cube-en.md
@@ -0,0 +1,148 @@
+The Point class of the three-dimensional space contains methods for rotations by an angle and for
+obtaining projections onto a plane. When obtaining projections, the distance from the point to the
+projection center is calculated. Point also contains a static method to compare two projections
+of points.
+
+{% capture collapsed_md %}
+```js
+class Point {
+ // point coordinates
+ constructor(x,y,z) {
+ this.x=x;
+ this.y=y;
+ this.z=z;
+ }
+ // rotate this point by an angle (deg) along
+ // axes (x,y,z) relative to the point (t0)
+ rotate(deg, t0) {
+ // functions to obtain sine and cosine of angle in radians
+ const sin = (deg) => Math.sin((Math.PI/180)*deg);
+ const cos = (deg) => Math.cos((Math.PI/180)*deg);
+ // calculate new coordinates of point using the formulas
+ // of the rotation matrix for three-dimensional space
+ let x,y,z;
+ // rotation along 'x' axis
+ y = t0.y+(this.y-t0.y)*cos(deg.x)-(this.z-t0.z)*sin(deg.x);
+ z = t0.z+(this.y-t0.y)*sin(deg.x)+(this.z-t0.z)*cos(deg.x);
+ this.y=y; this.z=z;
+ // rotation along 'y' axis
+ x = t0.x+(this.x-t0.x)*cos(deg.y)-(this.z-t0.z)*sin(deg.y);
+ z = t0.z+(this.x-t0.x)*sin(deg.y)+(this.z-t0.z)*cos(deg.y);
+ this.x=x; this.z=z;
+ // rotation along 'z' axis
+ x = t0.x+(this.x-t0.x)*cos(deg.z)-(this.y-t0.y)*sin(deg.z);
+ y = t0.y+(this.x-t0.x)*sin(deg.z)+(this.y-t0.y)*cos(deg.z);
+ this.x=x; this.y=y;
+ }
+ // get a projection of (type) from a distance (d)
+ // onto the plane of the observer screen (tv)
+ projection(type, tv, d) {
+ let proj = {};
+ // obtain a projection using experimental formulas
+ switch (type) {
+ case 'parallel': {
+ proj.x = this.x;
+ proj.y = this.y+(tv.y-this.z)/4;
+ break;
+ }
+ case 'perspective': {
+ proj.x = tv.x+d*(this.x-tv.x)/(this.z-tv.z+d);
+ proj.y = tv.y+d*(this.y-tv.y)/(this.z-tv.z+d);
+ break;
+ }
+ }
+ // calculate distance to projection center
+ proj.dist = Math.sqrt((this.x-tv.x)*(this.x-tv.x)
+ +(this.y-tv.y)*(this.y-tv.y)
+ +(this.z-tv.z+d)*(this.z-tv.z+d));
+ return proj;
+ }
+ // compare two projections of points (p1,p2),
+ // coordinates (x,y) should match
+ static pEquals(p1, p2) {
+ return Math.abs(p1.x-p2.x)<0.0001
+ && Math.abs(p1.y-p2.y)<0.0001;
+ }
+};
+```
+{% endcapture %}
+{%- include collapsed_block.html summary="class Point" content=collapsed_md -%}
+
+The Cube class contains a collection of vertices of the Point class and an array of faces. Each face is an
+array of 4 vertices, coming from the same point and going clockwise. The Cube contains methods for rotating
+all vertices by an angle and for obtaining projections of all faces onto a plane. When obtaining projections,
+the tilt of the face is calculated — this is the remoteness from the projection plane. The cube also contains
+two static methods for comparing two face projections: for defining the equidistant faces from the projection
+center and adjacent walls between neighboring cubes.
+
+{% capture collapsed_md %}
+```js
+class Cube {
+ // left upper near coordinate and size
+ constructor(x,y,z,size) {
+ // right lower distant coordinate
+ let xs=x+size,ys=y+size,zs=z+size;
+ let v={ // vertices
+ t000: new Point(x,y,z), // top
+ t001: new Point(x,y,zs), // top
+ t010: new Point(x,ys,z), // bottom
+ t011: new Point(x,ys,zs), // bottom
+ t100: new Point(xs,y,z), // top
+ t101: new Point(xs,y,zs), // top
+ t110: new Point(xs,ys,z), // bottom
+ t111: new Point(xs,ys,zs)};// bottom
+ this.vertices=v;
+ this.faces=[ // faces
+ [v.t000,v.t100,v.t110,v.t010], // front
+ [v.t000,v.t010,v.t011,v.t001], // left
+ [v.t000,v.t001,v.t101,v.t100], // upper
+ [v.t001,v.t011,v.t111,v.t101], // rear
+ [v.t100,v.t101,v.t111,v.t110], // right
+ [v.t010,v.t110,v.t111,v.t011]];// lower
+ }
+ // rotate vertices of the cube by an angle (deg)
+ // along axes (x,y,z) relative to the point (t0)
+ rotate(deg, t0) {
+ for (let vertex in this.vertices)
+ this.vertices[vertex].rotate(deg, t0);
+ }
+ // get projections of (type) from a distance (d)
+ // onto the plane of the observer screen (tv)
+ projection(type, tv, d) {
+ let proj = [];
+ for (let face of this.faces) {
+ // face projection, array of vertices
+ let p = [];
+ // cumulative remoteness of vertices
+ p.dist = 0;
+ // bypass the vertices of the face
+ for (let vertex of face) {
+ // obtain the projections of the vertices
+ let proj = vertex.projection(type, tv, d);
+ // accumulate the remoteness of vertices
+ p.dist+=proj.dist;
+ // add to array of vertices
+ p.push(proj);
+ }
+ // calculate face tilt, remoteness from the projection plane
+ p.clock = ((p[1].x-p[0].x)*(p[2].y-p[0].y)
+ -(p[1].y-p[0].y)*(p[2].x-p[0].x))<0;
+ proj.push(p);
+ }
+ return proj;
+ }
+ // compare two projections of faces (f1,f2), vertices
+ // should be equidistant from the center of projection
+ static pEquidistant(f1, f2) {
+ return Math.abs(f1.dist-f2.dist)<0.0001;
+ }
+ // compare two projections of faces (f1,f2), coordinates
+ // of points along the main diagonal (p0,p2) should match
+ static pAdjacent(f1, f2) {
+ return Point.pEquals(f1[0],f2[0])
+ && Point.pEquals(f1[2],f2[2]);
+ }
+};
+```
+{% endcapture %}
+{%- include collapsed_block.html summary="class Cube" content=collapsed_md -%}
diff --git a/jekyll_site/_includes/classes-point-cube-ru.md b/jekyll_site/_includes/classes-point-cube-ru.md
new file mode 100644
index 0000000..318e96d
--- /dev/null
+++ b/jekyll_site/_includes/classes-point-cube-ru.md
@@ -0,0 +1,146 @@
+Класс Точка трёхмерного пространства содержит методы для поворотов на угол и для получения проекций
+на плоскость. При получении проекций, вычисляется расстояние от точки до центра проекции. Точка также
+содержит статический метод для сравнения двух проекций точек.
+
+{% capture collapsed_md %}
+```js
+class Point {
+ // координаты точки
+ constructor(x,y,z) {
+ this.x=x;
+ this.y=y;
+ this.z=z;
+ }
+ // поворачиваем эту точку на угол (deg)
+ // по осям (x,y,z) относительно точки (t0)
+ rotate(deg, t0) {
+ // функции для получения синуса и косинуса угла в радианах
+ const sin = (deg) => Math.sin((Math.PI/180)*deg);
+ const cos = (deg) => Math.cos((Math.PI/180)*deg);
+ // получаем новые координаты точки по формулам
+ // матрицы поворота для трёхмерного пространства
+ let x,y,z;
+ // поворот по оси 'x'
+ y = t0.y+(this.y-t0.y)*cos(deg.x)-(this.z-t0.z)*sin(deg.x);
+ z = t0.z+(this.y-t0.y)*sin(deg.x)+(this.z-t0.z)*cos(deg.x);
+ this.y=y; this.z=z;
+ // поворот по оси 'y'
+ x = t0.x+(this.x-t0.x)*cos(deg.y)-(this.z-t0.z)*sin(deg.y);
+ z = t0.z+(this.x-t0.x)*sin(deg.y)+(this.z-t0.z)*cos(deg.y);
+ this.x=x; this.z=z;
+ // поворот по оси 'z'
+ x = t0.x+(this.x-t0.x)*cos(deg.z)-(this.y-t0.y)*sin(deg.z);
+ y = t0.y+(this.x-t0.x)*sin(deg.z)+(this.y-t0.y)*cos(deg.z);
+ this.x=x; this.y=y;
+ }
+ // получаем проекцию типа (type) с расстояния (d)
+ // на плоскость экрана наблюдателя (tv)
+ projection(type, tv, d) {
+ let proj = {};
+ // получаем проекцию по экспериментальным формулам
+ switch (type) {
+ case 'parallel': {
+ proj.x = this.x;
+ proj.y = this.y+(tv.y-this.z)/4;
+ break;
+ }
+ case 'perspective': {
+ proj.x = tv.x+d*(this.x-tv.x)/(this.z-tv.z+d);
+ proj.y = tv.y+d*(this.y-tv.y)/(this.z-tv.z+d);
+ break;
+ }
+ }
+ // вычисляем расстояние до центра проекции
+ proj.dist = Math.sqrt((this.x-tv.x)*(this.x-tv.x)
+ +(this.y-tv.y)*(this.y-tv.y)
+ +(this.z-tv.z+d)*(this.z-tv.z+d));
+ return proj;
+ }
+ // сравниваем две проекции точек (p1,p2),
+ // координаты (x,y) должны совпадать
+ static pEquals(p1, p2) {
+ return Math.abs(p1.x-p2.x)<0.0001
+ && Math.abs(p1.y-p2.y)<0.0001;
+ }
+};
+```
+{% endcapture %}
+{%- include collapsed_block.html summary="class Point" content=collapsed_md -%}
+
+Класс Куб содержит коллекцию вершин класса Точка и массив граней. Каждая грань — это массив из 4 вершин,
+выходящих из одной точки и идущих по часовой стрелке. Куб содержит методы для поворота всех вершин на угол
+и для получения проекций всех граней на плоскость. При получении проекций, вычисляется наклон грани — это
+удалённость от плоскости проекции. Куб также содержит два статических метода для сравнения двух проекций
+граней: для определения равноудалённых граней от центра проекции и смежных стенок между соседними кубиками.
+
+{% capture collapsed_md %}
+```js
+class Cube {
+ // левая верхняя ближняя координата и размер
+ constructor(x,y,z,size) {
+ // правая нижняя дальняя координата
+ let xs=x+size,ys=y+size,zs=z+size;
+ let v={ // вершины
+ t000: new Point(x,y,z), // верх
+ t001: new Point(x,y,zs), // верх
+ t010: new Point(x,ys,z), // низ
+ t011: new Point(x,ys,zs), // низ
+ t100: new Point(xs,y,z), // верх
+ t101: new Point(xs,y,zs), // верх
+ t110: new Point(xs,ys,z), // низ
+ t111: new Point(xs,ys,zs)};// низ
+ this.vertices=v;
+ this.faces=[ // грани
+ [v.t000,v.t100,v.t110,v.t010], // передняя
+ [v.t000,v.t010,v.t011,v.t001], // левая
+ [v.t000,v.t001,v.t101,v.t100], // верхняя
+ [v.t001,v.t011,v.t111,v.t101], // задняя
+ [v.t100,v.t101,v.t111,v.t110], // правая
+ [v.t010,v.t110,v.t111,v.t011]];// нижняя
+ }
+ // поворачиваем вершины куба на угол (deg)
+ // по осям (x,y,z) относительно точки (t0)
+ rotate(deg, t0) {
+ for (let vertex in this.vertices)
+ this.vertices[vertex].rotate(deg, t0);
+ }
+ // получаем проекции типа (type) с расстояния (d)
+ // на плоскость экрана наблюдателя (tv)
+ projection(type, tv, d) {
+ let proj = [];
+ for (let face of this.faces) {
+ // проекция грани, массив вершин
+ let p = [];
+ // кумулятивная удалённость вершин
+ p.dist = 0;
+ // обходим вершины грани
+ for (let vertex of face) {
+ // получаем проекции вершин
+ let proj = vertex.projection(type, tv, d);
+ // накапливаем удалённость вершин
+ p.dist+=proj.dist;
+ // добавляем в массив вершин
+ p.push(proj);
+ }
+ // вычисляем наклон грани, удалённость от плоскости проекции
+ p.clock = ((p[1].x-p[0].x)*(p[2].y-p[0].y)
+ -(p[1].y-p[0].y)*(p[2].x-p[0].x))<0;
+ proj.push(p);
+ }
+ return proj;
+ }
+ // сравниваем две проекции граней (f1,f2), вершины
+ // должны быть равноудалены от центра проекции
+ static pEquidistant(f1, f2) {
+ return Math.abs(f1.dist-f2.dist)<0.0001;
+ }
+ // сравниваем две проекции граней (f1,f2), координаты
+ // точек по главной диагонали (p0,p2) должны совпадать
+ static pAdjacent(f1, f2) {
+ return Point.pEquals(f1[0],f2[0])
+ && Point.pEquals(f1[2],f2[2]);
+ }
+};
+```
+{% endcapture %}
+{%- include collapsed_block.html summary="class Cube" content=collapsed_md -%}
diff --git a/jekyll_site/_includes/counters_body.html b/jekyll_site/_includes/counters_body.html
new file mode 100644
index 0000000..d559e92
--- /dev/null
+++ b/jekyll_site/_includes/counters_body.html
@@ -0,0 +1,2 @@
+
+
diff --git a/jekyll_site/_includes/counters_head.html b/jekyll_site/_includes/counters_head.html
new file mode 100644
index 0000000..6cf845f
--- /dev/null
+++ b/jekyll_site/_includes/counters_head.html
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/jekyll_site/_includes/volumetric-tetris-en.html b/jekyll_site/_includes/volumetric-tetris-en.html
new file mode 100644
index 0000000..a63dfbb
--- /dev/null
+++ b/jekyll_site/_includes/volumetric-tetris-en.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Level: , next level: , score:
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jekyll_site/_includes/volumetric-tetris-ru.html b/jekyll_site/_includes/volumetric-tetris-ru.html
new file mode 100644
index 0000000..f425b58
--- /dev/null
+++ b/jekyll_site/_includes/volumetric-tetris-ru.html
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+Уровень: , следующий уровень: , счёт:
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jekyll_site/css/pomodoro1.css b/jekyll_site/css/pomodoro1.css
new file mode 100644
index 0000000..a65b3fb
--- /dev/null
+++ b/jekyll_site/css/pomodoro1.css
@@ -0,0 +1,34 @@
+input {
+ accent-color: #888;
+ font-size: 100%;
+}
+
+input[type="number"] {
+ border: 1px solid #888;
+ border-radius: 4px;
+ max-width: 50px;
+}
+
+input[type="radio"] {
+ scale: 140%;
+}
+
+input[type="checkbox"] {
+ scale: 120%;
+}
+
+input:not([disabled]),
+input[type="radio"] + label {
+ cursor: pointer;
+}
+
+md-content input {
+ color: #1b5e20;
+}
+
+@media (max-width: 949px) {
+ .disabled-sm {
+ pointer-events: none;
+ color: #888;
+ }
+}
diff --git a/jekyll_site/en/2023/01/06/spinning-square-on-plane.md b/jekyll_site/en/2023/01/06/spinning-square-on-plane.md
new file mode 100644
index 0000000..0b29512
--- /dev/null
+++ b/jekyll_site/en/2023/01/06/spinning-square-on-plane.md
@@ -0,0 +1,161 @@
+---
+title: Spinning square on plane
+description: Let's write an algorithm in JavaScript to rotate a square by an angle around its center, repeat the high school program. We will use the Math class for...
+sections: [Linear algebra,Rotation matrix]
+tags: [javascript,canvas,geometry,graphics,image,picture,square]
+scripts: [/js/spinning-square.js,/js/spinning-square2.js]
+canonical_url: /en/2023/01/06/spinning-square-on-plane.html
+url_translated: /ru/2023/01/05/spinning-square-on-plane.html
+title_translated: Вращаем квадрат на плоскости
+date: 2023.01.06
+lang: en
+---
+
+Let's write an algorithm in JavaScript to rotate a square by an angle around its center, repeat the high
+school program. We will use the `Math` class for calculations, and Canvas for displaying the results.
+
+Development of thought, volumetric model: [Spinning cube in space]({{ '/en/2023/01/11/spinning-cube-in-space.html' | relative_url }}).
+
+### Point rotation on plane {#point-rotation-on-plane}
+
+We calculate the coordinates of the new point using the formulas of the rotation matrix for
+two-dimensional space. We rotate the point `t` relative to the point `t0` — we get the point `t'`.
+
+{% include image_svg.html src="/img/column-vector2d.svg" style="width: 246.793pt; height: 37.2836pt;"
+alt="&x'=x_0+(x-x_0)cos\varphi-(y-y_0)sin\varphi,&\\&y'=y_0+(x-x_0)sin\varphi+(y-y_0)cos\varphi.&\\" %}
+
+### Algorithm description {#algorithm-description}
+
+The origin of the coordinates is in the upper left corner, the coordinate axes are directed to the right and
+down. The central point for rotations `t0` is located in the center of the figure. A square is an array of
+four points-vertices. We bypass the array of points, rotate each of them by an angle, then link the points
+with lines and draw lines on the canvas. We renew the image at a frequency of 20 frames per second.
+
+### Implementation {#implementation}
+
+
+
+
+
+### HTML {#html1}
+
+```html
+
+```
+
+### JavaScript {#javascript1}
+
+```js
+'use strict';
+let canvas = document.getElementById('canvas');
+// original array of points-vertices of square
+let square = [{x:50,y:50},{x:50,y:250},{x:250,y:250},{x:250,y:50}];
+// figure center, we'll perform a rotation around it
+let t0 = {x:150, y:150};
+// rotation angle in degrees
+let deg = 1;
+```
+```js
+// figure rotation and image update
+function repaint() {
+ // rotate the original array of points by an angle
+ for (let i = 0; i < square.length; i++)
+ square[i] = rotateOnDegree(t0, square[i], deg);
+ // draw the current array of points
+ drawFigure(canvas, square);
+}
+```
+```js
+// rotate the point (t) by an angle (deg) relative to the point (t0)
+function rotateOnDegree(t0, t, deg) {
+ let t_new = {};
+ // convert angle of rotation from degrees to radians
+ let rad = (Math.PI / 180) * deg;
+ // calculate the coordinates of the new point using the formula
+ t_new.x = t0.x+(t.x-t0.x)*Math.cos(rad)-(t.y-t0.y)*Math.sin(rad);
+ t_new.y = t0.y+(t.x-t0.x)*Math.sin(rad)+(t.y-t0.y)*Math.cos(rad);
+ // return new point
+ return t_new;
+}
+```
+```js
+// draw a figure by points from an array
+function drawFigure(canvas, arr) {
+ let context = canvas.getContext('2d');
+ // clear the entire canvas
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // bypass the array of points and link them with lines
+ context.beginPath();
+ for (let i = 0; i < arr.length; i++)
+ if (i == 0)
+ context.moveTo(arr[i].x, arr[i].y);
+ else
+ context.lineTo(arr[i].x, arr[i].y);
+ context.closePath();
+ // draw lines on the canvas
+ context.lineWidth = 2.2;
+ context.strokeStyle = '#222';
+ context.stroke();
+}
+```
+```js
+// after loading the page, set the image refresh interval
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
+```
+
+## Spinning backwards {#spinning-backwards}
+
+Let's add one more point, which we'll rotate backwards. The point is distant from the center of the
+figure by a quarter of the length of the side of the square. let's shift the center of the square to
+this point — shift the array of its vertices. We will rotate the square itself clockwise, and its
+central point — counterclockwise. This code works in conjunction with the previous one.
+
+
+
+
+
+### HTML {#html2}
+
+```html
+
+```
+
+### JavaScript {#javascript2}
+
+```js
+'use strict';
+let canvas2 = document.getElementById('canvas2');
+// current array of points
+let square2 = [];
+// spinning point
+let t2 = {x:100, y:100};
+```
+```js
+// figure rotation and image update
+function repaint2() {
+ // rotate the point in the opposite direction
+ t2 = rotateOnDegree(t0, t2, -deg);
+ // bypass the points of the original array and shift
+ for (let i = 0; i < square.length; i++) {
+ // current point
+ square2[i] = {};
+ // shifting the point of the original array
+ square2[i].x = square[i].x - t0.x + t2.x;
+ square2[i].y = square[i].y - t0.y + t2.y;
+ }
+ // draw the current array of points
+ drawFigure(canvas2, square2);
+}
+```
+```js
+// after loading the page, set the image refresh interval
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint2,50));
+```
diff --git a/jekyll_site/en/2023/01/11/spinning-cube-in-space.md b/jekyll_site/en/2023/01/11/spinning-cube-in-space.md
new file mode 100644
index 0000000..0216bf7
--- /dev/null
+++ b/jekyll_site/en/2023/01/11/spinning-cube-in-space.md
@@ -0,0 +1,229 @@
+---
+title: Spinning cube in space
+description: We consider the difference between parallel and perspective projection. Both are widely used in practice for various purposes. In the previous example, we...
+sections: [Linear perspective,Rotation matrix,Experimental model]
+tags: [javascript,canvas,geometry,graphics,image,picture,square,cube]
+scripts: [/js/classes-point-cube.js,/js/spinning-cube.js,/js/spinning-cube2.js]
+styles: [/css/pomodoro1.css]
+canonical_url: /en/2023/01/11/spinning-cube-in-space.html
+url_translated: /ru/2023/01/10/spinning-cube-in-space.html
+title_translated: Вращаем куб в пространстве
+date: 2023.01.11
+lang: en
+---
+
+We consider the difference between parallel and perspective projection.
+Both are widely used in practice for various purposes. In the previous example, we
+[rotated square on plane]({{ '/en/2023/01/06/spinning-square-on-plane.html' | relative_url }})
+— we pass into three-dimensional space. Now, to display the rotation of a three-dimensional object
+on the screen plane, we first need to create a three-dimensional object, rotate it by an angle,
+draw a projection from it and display already the projection on the screen.
+
+Complicated model, many cubes: [Spinning spatial cross]({{ '/en/2023/01/16/spinning-spatial-cross.html' | relative_url }}).
+
+
+
+ Parallel projection
+
+
+
+ Perspective projection
+
+
+
+
+*Parallel projection* — the projection center is infinitely distant from the plane of the observer screen,
+dimensions of the objects look the same.
+
+*Perspective projection* — parallel lines converge in the center of the perspective,
+objects appear to shrink in the distance.
+
+## Experimental model {#experimental-model}
+
+Cube size 200, canvas size 300, origin of coordinates is in the upper left corner. The center of the figure
+is in the middle of the canvas. The `X` axis is directed to the right, the `Y` axis is directed downwards,
+the `Z` axis is directed to the distance. The rotation is performed sequentially around all three axes: first
+around the `X` axis, then around the `Y` axis and then around the `Z` axis. Model settings can be controlled,
+for example, you can switch off redundant rotation around the axes and change the position of the projection
+center onto the observer screen.
+
+
+
+
+
+
+## Point rotation in space {#point-rotation-in-space}
+
+We calculate the new coordinates of the point using the formulas of the rotation matrix for
+three-dimensional space. We rotate the point `t` relative to the point `t0` — we get the point `t'`.
+
+*Rotation along `X` axis.*
+
+{% include image_svg.html src="/img/column-vector3dx.svg" style="width: 242.619pt; height: 59.0768pt;"
+alt="&x'=x,&\\&y'=y_0+(y-y_0)cos\varphi-(z-z_0)sin\varphi,&\\&z'=z_0+(y-y_0)sin\varphi+(z-z_0)cos\varphi.&\\" %}
+
+*Rotation along `Y` axis.*
+
+{% include image_svg.html src="/img/column-vector3dy.svg" style="width: 246.251pt; height: 59.0768pt;"
+alt="&x'=x_0+(x-x_0)cos\varphi-(z-z_0)sin\varphi,&\\&y'=y,&\\&z'=z_0+(x-x_0)sin\varphi+(z-z_0)cos\varphi.&\\" %}
+
+*Rotation along `Z` axis.*
+
+{% include image_svg.html src="/img/column-vector3dz.svg" style="width: 246.793pt; height: 55.4753pt;"
+alt="&x'=x_0+(x-x_0)cos\varphi-(y-y_0)sin\varphi,&\\&y'=y_0+(x-x_0)sin\varphi+(y-y_0)cos\varphi,&\\&z'=z.&\\" %}
+
+## Point projection {#point-projection}
+
+Experimental formulas with the possibility of shifting the projection center `d0` on the observer
+screen `tv`. We map the point of space `t` to the plane of the screen — we get the point `t'`.
+
+*Parallel projection.*
+
+{% include image_svg.html src="/img/oblique-projection.svg" style="width: 123.97pt; height: 37.2836pt;"
+alt="&x'=x,&\\&y'=y+(y_v-z)/4.&\\" %}
+
+*Perspective projection.*
+
+{% include image_svg.html src="/img/central-projection.svg" style="width: 231.924pt; height: 37.2836pt;"
+alt="&x'=x_v+d_0\cdot(x-x_v)/(z-z_v+d_0),&\\&y'=y_v+d_0\cdot(y-y_v)/(z-z_v+d_0).&\\" %}
+
+*Distance from the point to the projection center.*
+
+{% include image_svg.html src="/img/euclidean-distance.svg" style="width: 319.911pt; height: 17.9328pt;"
+alt="d(t,d_0)=\sqrt{(x-x_v)^2+(y-y_v)^2+(z-z_v+d_0)^2}." %}
+
+## Face sorting {#face-sorting}
+
+When creating a cube, we set the vertices of each face clockwise. When obtaining a projection, we
+substitute three consecutive vertices into the equation of a line, to determine the tilt of the
+face and its remoteness from the projection plane.
+
+*Equation of the line, that passes through two points.*
+
+{% include image_svg.html src="/img/linear-equation.svg" style="width: 137.171pt; height: 35.3194pt;"
+alt="{(x-x_1)\over(y-y_1)}={(x_2-x_1)\over(y_2-y_1)}." %}
+
+## Algorithm description {#algorithm-description}
+
+First, we bypass the vertices of the cube and rotate them by an angle relative to the center point. Then
+we bypass the faces of the cube and get projections of the vertices included in them. After that, we sort
+the projections of the faces by remoteness. Then we draw projections on the plane — we link the points
+with lines. We draw with a translucent color first the far faces and atop them the near ones, so that
+the far faces can be seen through the near ones.
+
+At each step of displaying the figure, we repeat the sorting of the faces by remoteness, since
+with a change in the angle of rotation, the coordinates shift, and the near faces become far.
+
+## Implementation in JavaScript {#implementation-in-javascript}
+
+{% include classes-point-cube-en.md -%}
+
+Create an object and draw two projections on the plane.
+
+```js
+'use strict';
+// we will draw two pictures at once, there will be
+// one object, and there will be many projections
+const canvas1 = document.getElementById('canvas1');
+const canvas2 = document.getElementById('canvas2');
+// create an object
+const cube = new Cube(50,50,50,200);
+// figure center, we'll perform a rotation around it
+const t0 = new Point(150,150,150);
+// remoteness of the projection center
+const d = 300;
+// observer screen position
+const tv = new Point(150,150,80);
+// rotation angle in degrees
+const deg = {x:0,y:1,z:0};
+```
+```js
+// figure rotation and image update
+function repaint() {
+ cube.rotate(deg, t0);
+ // draw parallel projection
+ drawFigure(canvas1, cube.projection('parallel', tv));
+ // draw perspective projection
+ drawFigure(canvas2, cube.projection('perspective', tv, d));
+}
+```
+```js
+// draw a figure by points from an array
+function drawFigure(canvas, proj) {
+ let context = canvas.getContext('2d');
+ // sort the faces by their tilt
+ proj.sort((a,b) => b.clock-a.clock);
+ // clear the entire canvas
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // bypass the array of cube faces
+ for (let i = 0; i < proj.length; i++) {
+ // bypass the array of points and link them with lines
+ context.beginPath();
+ for (let j = 0; j < proj[i].length; j++) {
+ if (j == 0) {
+ context.moveTo(proj[i][j].x, proj[i][j].y);
+ } else {
+ context.lineTo(proj[i][j].x, proj[i][j].y);
+ }
+ }
+ context.closePath();
+ // draw the face of the cube along with the edges
+ context.lineWidth = 2.2;
+ context.lineJoin = 'round';
+ context.fillStyle = '#fff9';
+ context.strokeStyle = '#222';
+ context.fill();
+ context.stroke();
+ }
+}
+```
+```js
+// after loading the page, set the image refresh interval
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
+```
diff --git a/jekyll_site/en/2023/01/16/spinning-spatial-cross.md b/jekyll_site/en/2023/01/16/spinning-spatial-cross.md
new file mode 100644
index 0000000..9d6a20c
--- /dev/null
+++ b/jekyll_site/en/2023/01/16/spinning-spatial-cross.md
@@ -0,0 +1,275 @@
+---
+title: Spinning spatial cross
+description: We are writing an algorithm for rotating a three-dimensional figure by an angle around its center along all three axes at once. In the previous example...
+sections: [Volumetric figures,Rotation matrix,Experimental model]
+tags: [javascript,canvas,geometry,matrix,graphics,image,picture,square,cube]
+scripts: [/js/classes-point-cube.js,/js/spinning-spatial-cross.js,/js/spinning-spatial-cross2.js]
+styles: [/css/pomodoro1.css]
+canonical_url: /en/2023/01/16/spinning-spatial-cross.html
+url_translated: /ru/2023/01/15/spinning-spatial-cross.html
+title_translated: Вращаем пространственный крест
+date: 2023.01.16
+lang: en
+---
+
+We are writing an algorithm for rotating a three-dimensional figure by an angle
+around its center along all three axes at once. In the previous example, we
+[rotated cube in space]({{ '/en/2023/01/11/spinning-cube-in-space.html' | relative_url }})
+— now there are a lot of cubes, the algorithm is almost the same and we use the same formulas.
+We draw two variants of the figure: *spatial cross* and *cross-cube* in two types of projections,
+consider the difference.
+
+Testing the experimental interface: [Volumetric tetris]({{ '/en/2023/01/22/volumetric-tetris.html' | relative_url }}).
+
+## Spatial cross {#spatial-cross}
+
+
+
+ Parallel projection
+
+
+
+ Perspective projection
+
+
+
+
+## Cross-cube {#cross-cube}
+
+
+
+ Parallel projection
+
+
+
+ Perspective projection
+
+
+
+
+*Parallel projection* — all cubes are the same size.
+
+*Perspective projection* — the cubes look shrinking in the distance.
+
+## Experimental model {#experimental-model}
+
+Slightly complicated version from the previous example — now there are a lot of cubes. In addition to the
+previous settings there can be changed: figure variant — *spatial cross* or *cross-cube*, face sorting
+direction — *linear perspective* or *reverse perspective* and transparency of the cube walls.
+
+
+
+
+
+
+Variant of the figure:
+
+Perspective projection:
+
+
+
+## Algorithm description {#algorithm-description}
+
+We prepare a matrix of zeros and ones, where one means a cube in a certain place of the figure. Then we
+bypass this matrix and fill in the array of cubes with the corresponding coordinates of the vertices. After
+that, we start the rotation along all three axes at once. At each step, we bypass the array of cubes and get
+projections of their faces. Then we sort the array of faces by remoteness from the projection center, bypass
+this array and throw away the same pairs from it — these are the adjacent walls between neighboring cubes
+inside the figure. After that we draw cube faces with a translucent color — first the distant and then the
+near ones, so that the distant faces can be seen through the near ones.
+
+## Implementation in JavaScript {#implementation-in-javascript}
+
+{% include classes-point-cube-en.md -%}
+
+Create objects according to templates and draw their projections on the plane.
+
+```js
+'use strict';
+// matrices-templates for cubes
+const shape1 = [ // spatial cross
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
+ [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]]];
+const shape2 = [ // cross-cube
+ [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
+ [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
+ [[1,1,1,1,1], [1,0,0,0,1], [1,0,0,0,1], [1,0,0,0,1], [1,1,1,1,1]],
+ [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
+ [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]]];
+// cube size, number of cubes in a row, indent
+const size = 40, row = 5, gap = 50;
+// arrays for cubes
+const cubes1 = [], cubes2 = [];
+// bypass the matrices, fill the arrays with cubes
+for (let x=0; xMath.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
+ // sort the faces by remoteness from the projection center
+ perspective.sort((a,b)=>b.dist-a.dist);
+ // draw parallel projection
+ drawFigure(cnv1, parallel);
+ // draw perspective projection
+ drawFigure(cnv2, perspective);
+}
+```
+```js
+// do not draw adjacent walls between neighboring cubes
+function noAdjacent(array) {
+ // sort the faces by remoteness
+ array.sort((a,b) => b.dist-a.dist);
+ // remove the adjacent walls between cubes
+ for (let i=0, j=1; isetInterval(repaint,50));
+```
diff --git a/jekyll_site/en/2023/01/22/volumetric-tetris.md b/jekyll_site/en/2023/01/22/volumetric-tetris.md
new file mode 100644
index 0000000..7ae4176
--- /dev/null
+++ b/jekyll_site/en/2023/01/22/volumetric-tetris.md
@@ -0,0 +1,53 @@
+---
+title: Volumetric tetris
+description: General educational game in the broad meaning of this word. When learning programming languages, it is recommended to write your own version first and then...
+sections: [Logical game,Experimental interface]
+tags: [javascript,canvas,game,puzzle,geometry,matrix,graphics,square,cube,3d,three-dimensional]
+scripts: [/js/classes-point-cube.js,/js/tetris-figures.js,/js/tetris-model.js,/js/tetris-controller.js,/js/tetris-view.js]
+styles: [/css/pomodoro1.css]
+canonical_url: /en/2023/01/22/volumetric-tetris.html
+url_translated: /ru/2023/01/21/volumetric-tetris.html
+title_translated: Объёмный тетрис
+date: 2023.01.22
+lang: en
+---
+
+General educational game in the broad meaning of this word. When learning programming languages, it is
+recommended to write your own version first and then use it to demonstrate and test other software or
+hardware. The three-dimensional interface is written in JavaScript Canvas — the logic of the game itself
+is two-dimensional.
+
+Description of graphics algorithm: [Spinning cube in space]({{ '/en/2023/01/11/spinning-cube-in-space.html' | relative_url }}).
+
+## Experimental interface {#experimental-interface}
+
+Turned off by default — you can just play Tetris. In addition to the flat version, two volumetric variants
+are added: *parallel projection* and *perspective projection* — parameters for each of them can be changed.
+For perspective projection: you can change the position of the observer screen and the remoteness of the
+projection source. The observer looks at the center of the image, and the center of the projection is remote
+at a distance, comparable to the size of the playing field. For parallel projection: you can change the
+vertical position. For both projections: you can rotate the playing field along all three axes. The central
+point for rotations — is the central lower far point of the field. For all variants of the image: the size
+of the cube — 32, the size of the square — 30 and the indent — 2. The origin of coordinates is located at
+the upper left point, the axes are directed: `X` to the right, `Y` downwards and `Z` to the distance.
+
+*Usage example:* start the game, collect a certain number of figures on the field, then pause the game, and
+switch between the variants of the three-dimensional image, rotate the field with figures, change the settings.
+
+{% include volumetric-tetris-en.html -%}
+
+## Gaming process {#gaming-process}
+
+Controls: keyboard buttons with arrows — right, left, up, down and the button `pause`.
+
+Game points are awarded for fully collected rows of the elements of the figures. The number of points scored
+depends on the number of rows collected, 10 points for each row if there are 10 cubes in a row, and multiply
+increases, if collected at the same time: 2 lines — by 3 times, 3 lines — by 5 times, 4 lines — by 10 times.
+
+Game feature: the collected lines first blink and then disappear, while the gaming process is not suspended
+for this time — the current figure continues to fall.
+
+Level increases when collecting 10 completed rows, that is 100 points, if there are 10 cubes in a row. At each
+new level, the speed of the figures increases and reaches its maximum at level 21. In snail mode, the speed
+increases 5 times slower and reaches a maximum at level 104. The current speed is displayed above the playing
+field as a `meter` indicator.
diff --git a/jekyll_site/en/index.md b/jekyll_site/en/index.md
new file mode 100644
index 0000000..49d234f
--- /dev/null
+++ b/jekyll_site/en/index.md
@@ -0,0 +1,48 @@
+---
+title: Code with comments
+description: Notes about programming with code snippets and comments. Problem solutions and solution descriptions.
+sections: [Problem solutions and solution descriptions]
+tags: [javascript,canvas,geometry,matrix,algorithms,implementation,graphics,images,pictures,square,cube]
+canonical_url: /en/
+url_translated: /ru/
+title_translated: Код с комментариями
+lang: en
+---
+
+{%- assign articles = "" | split: "" %}
+{%- assign articles = articles | push: "Volumetric tetris" %}
+{%- capture article_brief %}
+General educational game in the broad meaning of this word. When learning programming languages, it is
+recommended to write your own version first and then use it to demonstrate and test other software or
+hardware. The three-dimensional interface is written in JavaScript Canvas — the logic of the game itself
+is two-dimensional.
+{%- endcapture %}
+{%- assign articles = articles | push: article_brief %}
+{%- assign articles = articles | push: "Spinning spatial cross" %}
+{%- capture article_brief %}
+We are writing an algorithm for rotating a three-dimensional figure by an angle around its center along all three
+axes at once. In the previous example, we rotated cube in space — now there are a lot of cubes, the algorithm is
+almost the same and we use the same formulas. We draw two variants of the figure: *spatial cross* and *cross-cube*
+in two types of projections, consider the difference.
+{%- endcapture %}
+{%- assign articles = articles | push: article_brief %}
+{%- assign articles = articles | push: "Spinning cube in space" %}
+{%- capture article_brief %}
+We consider the difference between parallel and perspective projection. Both are widely used in practice for various
+purposes. In the previous example, we rotated square on plane — we pass into three-dimensional space. Now, to display
+the rotation of a three-dimensional object on the screen plane, we first need to create a three-dimensional object,
+rotate it by an angle, draw a projection from it and display already the projection on the screen.
+{%- endcapture %}
+{%- assign articles = articles | push: article_brief %}
+{%- assign articles = articles | push: "Spinning square on plane" %}
+{%- capture article_brief %}
+Let's write an algorithm in JavaScript to rotate a square by an angle around its center, repeat the high
+school program. We will use the `Math` class for calculations, and Canvas for displaying the results.
+
+The origin of the coordinates is in the upper left corner, the coordinate axes are directed to the right and
+down. The central point for rotations `t0` is located in the center of the figure. A square is an array of
+four points-vertices. We bypass the array of points, rotate each of them by an angle, then link the points
+with lines and draw lines on the canvas. We renew the image at a frequency of 20 frames per second.
+{%- endcapture %}
+{%- assign articles = articles | push: article_brief %}
+{%- include main_page.html articles = articles -%}
diff --git a/jekyll_site/img/central-projection.svg b/jekyll_site/img/central-projection.svg
new file mode 100644
index 0000000..c845ab9
--- /dev/null
+++ b/jekyll_site/img/central-projection.svg
@@ -0,0 +1,75 @@
+
diff --git a/jekyll_site/img/column-vector2d.svg b/jekyll_site/img/column-vector2d.svg
new file mode 100644
index 0000000..f9ad533
--- /dev/null
+++ b/jekyll_site/img/column-vector2d.svg
@@ -0,0 +1,80 @@
+
diff --git a/jekyll_site/img/column-vector3dx.svg b/jekyll_site/img/column-vector3dx.svg
new file mode 100644
index 0000000..5f7c52b
--- /dev/null
+++ b/jekyll_site/img/column-vector3dx.svg
@@ -0,0 +1,86 @@
+
diff --git a/jekyll_site/img/column-vector3dy.svg b/jekyll_site/img/column-vector3dy.svg
new file mode 100644
index 0000000..bda2c38
--- /dev/null
+++ b/jekyll_site/img/column-vector3dy.svg
@@ -0,0 +1,86 @@
+
diff --git a/jekyll_site/img/column-vector3dz.svg b/jekyll_site/img/column-vector3dz.svg
new file mode 100644
index 0000000..fbb9e4c
--- /dev/null
+++ b/jekyll_site/img/column-vector3dz.svg
@@ -0,0 +1,86 @@
+
diff --git a/jekyll_site/img/euclidean-distance.svg b/jekyll_site/img/euclidean-distance.svg
new file mode 100644
index 0000000..fe53f04
--- /dev/null
+++ b/jekyll_site/img/euclidean-distance.svg
@@ -0,0 +1,60 @@
+
diff --git a/jekyll_site/img/linear-equation.svg b/jekyll_site/img/linear-equation.svg
new file mode 100644
index 0000000..2d32f42
--- /dev/null
+++ b/jekyll_site/img/linear-equation.svg
@@ -0,0 +1,46 @@
+
diff --git a/jekyll_site/img/oblique-projection.svg b/jekyll_site/img/oblique-projection.svg
new file mode 100644
index 0000000..c6fb608
--- /dev/null
+++ b/jekyll_site/img/oblique-projection.svg
@@ -0,0 +1,40 @@
+
diff --git a/jekyll_site/js/classes-point-cube.js b/jekyll_site/js/classes-point-cube.js
new file mode 100644
index 0000000..def25cb
--- /dev/null
+++ b/jekyll_site/js/classes-point-cube.js
@@ -0,0 +1,135 @@
+// © Головин Г.Г., Код с комментариями, 2023
+'use strict';
+// Класс Точка трёхмерного пространства содержит методы для поворотов на угол и для получения проекций
+// на плоскость. При получении проекций, вычисляется расстояние от точки до центра проекции. Точка также
+// содержит статический метод для сравнения двух проекций точек.
+class Point {
+ // координаты точки
+ constructor(x,y,z) {
+ this.x=x;
+ this.y=y;
+ this.z=z;
+ }
+ // поворачиваем эту точку на угол (deg)
+ // по осям (x,y,z) относительно точки (t0)
+ rotate(deg, t0) {
+ // функции для получения синуса и косинуса угла в радианах
+ const sin = (deg) => Math.sin((Math.PI/180)*deg);
+ const cos = (deg) => Math.cos((Math.PI/180)*deg);
+ // получаем новые координаты точки по формулам
+ // матрицы поворота для трёхмерного пространства
+ let x,y,z;
+ // поворот по оси 'x'
+ y = t0.y+(this.y-t0.y)*cos(deg.x)-(this.z-t0.z)*sin(deg.x);
+ z = t0.z+(this.y-t0.y)*sin(deg.x)+(this.z-t0.z)*cos(deg.x);
+ this.y=y; this.z=z;
+ // поворот по оси 'y'
+ x = t0.x+(this.x-t0.x)*cos(deg.y)-(this.z-t0.z)*sin(deg.y);
+ z = t0.z+(this.x-t0.x)*sin(deg.y)+(this.z-t0.z)*cos(deg.y);
+ this.x=x; this.z=z;
+ // поворот по оси 'z'
+ x = t0.x+(this.x-t0.x)*cos(deg.z)-(this.y-t0.y)*sin(deg.z);
+ y = t0.y+(this.x-t0.x)*sin(deg.z)+(this.y-t0.y)*cos(deg.z);
+ this.x=x; this.y=y;
+ }
+ // получаем проекцию типа (type) с расстояния (d)
+ // на плоскость экрана наблюдателя (tv)
+ projection(type, tv, d) {
+ let proj = {};
+ // получаем проекцию по экспериментальным формулам
+ switch (type) {
+ case 'parallel': {
+ proj.x = this.x;
+ proj.y = this.y+(tv.y-this.z)/4;
+ break;
+ }
+ case 'perspective': {
+ proj.x = tv.x+d*(this.x-tv.x)/(this.z-tv.z+d);
+ proj.y = tv.y+d*(this.y-tv.y)/(this.z-tv.z+d);
+ break;
+ }
+ }
+ // вычисляем расстояние до центра проекции
+ proj.dist = Math.sqrt((this.x-tv.x)*(this.x-tv.x)
+ +(this.y-tv.y)*(this.y-tv.y)
+ +(this.z-tv.z+d)*(this.z-tv.z+d));
+ return proj;
+ }
+ // сравниваем две проекции точек (p1,p2),
+ // координаты (x,y) должны совпадать
+ static pEquals(p1, p2) {
+ return Math.abs(p1.x-p2.x)<0.0001
+ && Math.abs(p1.y-p2.y)<0.0001;
+ }
+};
+// Класс Куб содержит коллекцию вершин класса Точка и массив граней. Каждая грань — это массив из 4 вершин,
+// выходящих из одной точки и идущих по часовой стрелке. Куб содержит методы для поворота всех вершин на угол
+// и для получения проекций всех граней на плоскость. При получении проекций, вычисляется наклон грани — это
+// удалённость от плоскости проекции. Куб также содержит два статических метода для сравнения двух проекций
+// граней: для определения равноудалённых граней от центра проекции и смежных стенок между соседними кубиками.
+class Cube {
+ // левая верхняя ближняя координата и размер
+ constructor(x,y,z,size) {
+ // правая нижняя дальняя координата
+ let xs=x+size,ys=y+size,zs=z+size;
+ let v={ // вершины
+ t000: new Point(x,y,z), // верх
+ t001: new Point(x,y,zs), // верх
+ t010: new Point(x,ys,z), // низ
+ t011: new Point(x,ys,zs), // низ
+ t100: new Point(xs,y,z), // верх
+ t101: new Point(xs,y,zs), // верх
+ t110: new Point(xs,ys,z), // низ
+ t111: new Point(xs,ys,zs)};// низ
+ this.vertices=v;
+ this.faces=[ // грани
+ [v.t000,v.t100,v.t110,v.t010], // передняя
+ [v.t000,v.t010,v.t011,v.t001], // левая
+ [v.t000,v.t001,v.t101,v.t100], // верхняя
+ [v.t001,v.t011,v.t111,v.t101], // задняя
+ [v.t100,v.t101,v.t111,v.t110], // правая
+ [v.t010,v.t110,v.t111,v.t011]];// нижняя
+ }
+ // поворачиваем вершины куба на угол (deg)
+ // по осям (x,y,z) относительно точки (t0)
+ rotate(deg, t0) {
+ for (let vertex in this.vertices)
+ this.vertices[vertex].rotate(deg, t0);
+ }
+ // получаем проекции типа (type) с расстояния (d)
+ // на плоскость экрана наблюдателя (tv)
+ projection(type, tv, d) {
+ let proj = [];
+ for (let face of this.faces) {
+ // проекция грани, массив вершин
+ let p = [];
+ // кумулятивная удалённость вершин
+ p.dist = 0;
+ // обходим вершины грани
+ for (let vertex of face) {
+ // получаем проекции вершин
+ let proj = vertex.projection(type, tv, d);
+ // накапливаем удалённость вершин
+ p.dist+=proj.dist;
+ // добавляем в массив вершин
+ p.push(proj);
+ }
+ // вычисляем наклон грани, удалённость от плоскости проекции
+ p.clock = ((p[1].x-p[0].x)*(p[2].y-p[0].y)
+ -(p[1].y-p[0].y)*(p[2].x-p[0].x))<0;
+ proj.push(p);
+ }
+ return proj;
+ }
+ // сравниваем две проекции граней (f1,f2), вершины
+ // должны быть равноудалены от центра проекции
+ static pEquidistant(f1, f2) {
+ return Math.abs(f1.dist-f2.dist)<0.0001;
+ }
+ // сравниваем две проекции граней (f1,f2), координаты
+ // точек по главной диагонали (p0,p2) должны совпадать
+ static pAdjacent(f1, f2) {
+ return Point.pEquals(f1[0],f2[0])
+ && Point.pEquals(f1[2],f2[2]);
+ }
+};
diff --git a/jekyll_site/js/spinning-cube.js b/jekyll_site/js/spinning-cube.js
new file mode 100644
index 0000000..bf8fcc8
--- /dev/null
+++ b/jekyll_site/js/spinning-cube.js
@@ -0,0 +1,57 @@
+// © Головин Г.Г., Код с комментариями, 2023
+'use strict';
+// рисовать будем сразу две картинки,
+// объект будет один, а проекций будет много
+const canvas1 = document.getElementById('canvas1');
+const canvas2 = document.getElementById('canvas2');
+// создаём объект
+const cube = new Cube(50,50,50,200);
+// центр фигуры, вокруг него будем выполнять поворот
+const t0 = new Point(150,150,150);
+// удалённость центра проекции
+const d = 300;
+// положение экрана наблюдателя
+const tv = new Point(150,150,80);
+// угол поворота в градусах
+const deg = {x:0,y:1,z:0};
+
+// поворот фигуры и обновление изображения
+function repaint() {
+ cube.rotate(deg, t0);
+ // рисуем параллельную проекцию
+ drawFigure(canvas1, cube.projection('parallel', tv));
+ // рисуем перспективную проекцию
+ drawFigure(canvas2, cube.projection('perspective', tv, d));
+}
+
+// рисуем фигуру по точкам из массива
+function drawFigure(canvas, proj) {
+ let context = canvas.getContext('2d');
+ // сортируем грани по их наклону
+ proj.sort((a,b) => b.clock-a.clock);
+ // очищаем весь холст целиком
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // обходим массив граней куба
+ for (let i = 0; i < proj.length; i++) {
+ // обходим массив точек и соединяем их линиями
+ context.beginPath();
+ for (let j = 0; j < proj[i].length; j++) {
+ if (j == 0) {
+ context.moveTo(proj[i][j].x, proj[i][j].y);
+ } else {
+ context.lineTo(proj[i][j].x, proj[i][j].y);
+ }
+ }
+ context.closePath();
+ // рисуем грань куба вместе с рёбрами
+ context.lineWidth = 2.2;
+ context.lineJoin = 'round';
+ context.fillStyle = '#fff9';
+ context.strokeStyle = '#222';
+ context.fill();
+ context.stroke();
+ }
+}
+
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
diff --git a/jekyll_site/js/spinning-cube2.js b/jekyll_site/js/spinning-cube2.js
new file mode 100644
index 0000000..dcb0aae
--- /dev/null
+++ b/jekyll_site/js/spinning-cube2.js
@@ -0,0 +1,55 @@
+// © Головин Г.Г., Экспериментальная модель, 2023
+'use strict';
+let d3=300,tv3={x:150,y:150,z:60};
+let deg2={x:1,y:1,z:1},show=false;
+// обработчики событий в форме
+function changeAxis(val,caller) {
+ deg2[val]=0+caller.target.checked;
+}
+function changeDistance(caller) {
+ d3=caller.target.valueAsNumber;
+}
+function changeTv(val,caller) {
+ tv3[val]=caller.target.valueAsNumber;
+}
+function showCenter(caller) {
+ show=caller.target.checked;
+}
+const cube3 = new Cube(50,50,50,200);
+const canvas3 = document.getElementById('canvas3');
+// перетаскивание центральной точки мышью
+let msBtnPressed = false;
+canvas3.onmouseup = ()=> msBtnPressed=false;
+canvas3.onmousedown = (caller)=> {
+ msBtnPressed=true;
+ canvas3.onmousemove(caller);
+}
+canvas3.onmousemove = function(caller) {
+ if (msBtnPressed && show) {
+ tv3.x=caller.offsetX;
+ tv3.y=caller.offsetY;
+ document.getElementById('rangeX').value=caller.offsetX;
+ document.getElementById('resultX').value=caller.offsetX;
+ document.getElementById('rangeY').value=caller.offsetY;
+ document.getElementById('resultY').value=caller.offsetY;
+ }
+}
+// поворот фигуры и обновление изображения
+function repaint3() {
+ cube3.rotate(deg2, t0);
+ // рисуем перспективную проекцию
+ drawFigure(canvas3, cube3.projection('perspective', tv3, d3));
+ // центральная точка перспективной проекции
+ if (show) centerPoint(canvas3);
+}
+// центральная точка перспективной проекции
+function centerPoint(canvas) {
+ const context = canvas.getContext('2d');
+ context.beginPath();
+ context.lineWidth = 2.2;
+ context.strokeStyle = '#222';
+ context.arc(tv3.x, tv3.y, 5.5, 0, 2*Math.PI);
+ context.stroke();
+}
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint3,50));
diff --git a/jekyll_site/js/spinning-spatial-cross.js b/jekyll_site/js/spinning-spatial-cross.js
new file mode 100644
index 0000000..8c0d8e9
--- /dev/null
+++ b/jekyll_site/js/spinning-spatial-cross.js
@@ -0,0 +1,116 @@
+// © Головин Г.Г., Код с комментариями, 2023
+'use strict';
+// матрицы-шаблоны для кубиков
+const shape1 = [ // пространственный крест
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
+ [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]],
+ [[0,0,0,0,0], [0,0,0,0,0], [0,0,1,0,0], [0,0,0,0,0], [0,0,0,0,0]]];
+const shape2 = [ // крест-куб
+ [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]],
+ [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
+ [[1,1,1,1,1], [1,0,0,0,1], [1,0,0,0,1], [1,0,0,0,1], [1,1,1,1,1]],
+ [[0,0,1,0,0], [0,0,0,0,0], [1,0,0,0,1], [0,0,0,0,0], [0,0,1,0,0]],
+ [[0,0,1,0,0], [0,0,1,0,0], [1,1,1,1,1], [0,0,1,0,0], [0,0,1,0,0]]];
+// размер кубика, количество кубиков в ряду, отступ
+const size = 40, row = 5, gap = 50;
+// массивы для кубиков
+const cubes1 = [], cubes2 = [];
+// обходим матрицы, заполняем массивы кубиками
+for (let x=0; xMath.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
+ // сортируем грани по удалённости от центра проекции
+ perspective.sort((a,b)=>b.dist-a.dist);
+ // рисуем параллельную проекцию
+ drawFigure(cnv1, parallel);
+ // рисуем перспективную проекцию
+ drawFigure(cnv2, perspective);
+}
+
+// смежные стенки между соседними кубиками не рисуем
+function noAdjacent(array) {
+ // сортируем грани по удалённости
+ array.sort((a,b) => b.dist-a.dist);
+ // удаляем смежные стенки между кубиками
+ for (let i=0, j=1; isetInterval(repaint,50));
diff --git a/jekyll_site/js/spinning-spatial-cross2.js b/jekyll_site/js/spinning-spatial-cross2.js
new file mode 100644
index 0000000..6ff5b98
--- /dev/null
+++ b/jekyll_site/js/spinning-spatial-cross2.js
@@ -0,0 +1,96 @@
+// © Головин Г.Г., Экспериментальная модель, 2023
+'use strict';
+let d5=300,tv5={x:150,y:150,z:125},show=false;
+let deg2={x:1,y:1,z:1};
+let sortOrder=true,alpha=20,first=false;
+// обработчики событий в форме
+function changeAxis(val,caller) {
+ deg2[val]=0+caller.target.checked;
+}
+function changeDistance(caller) {
+ d5=caller.target.valueAsNumber;
+}
+function changeTv(val,caller) {
+ tv5[val]=caller.target.valueAsNumber;
+}
+function showCenter(caller) {
+ show=caller.target.checked;
+}
+function changeFigure(caller) {
+ if (caller.target.value=="first") first=true;
+ if (caller.target.value=="second") first=false;
+}
+function changeOrder(caller) {
+ if (caller.target.value=="linear") sortOrder=true;
+ if (caller.target.value=="reverse") sortOrder=false;
+}
+function changeAlpha(caller) {
+ alpha=caller.target.valueAsNumber;
+}
+const canvas5 = document.getElementById('canvas5');
+// перетаскивание центральной точки мышью
+let msBtnPressed = false;
+canvas5.onmouseup = ()=> msBtnPressed=false;
+canvas5.onmousedown = (caller)=> {
+ msBtnPressed=true;
+ canvas5.onmousemove(caller);
+}
+canvas5.onmousemove = (caller)=> {
+ if (msBtnPressed && show) {
+ tv5.x=caller.offsetX;
+ tv5.y=caller.offsetY;
+ document.getElementById('rangeX').value=caller.offsetX;
+ document.getElementById('resultX').value=caller.offsetX;
+ document.getElementById('rangeY').value=caller.offsetY;
+ document.getElementById('resultY').value=caller.offsetY;
+ }
+}
+// массивы для кубиков
+const cubes5a = [], cubes5b = [];
+// обходим матрицы, заполняем массивы кубиками
+for (let x=0; xb.dist-a.dist);
+ // сортировка в обратном порядке
+ if (!sortOrder) proj.reverse();
+ // рисуем перспективную проекцию
+ drawFigure(canvas, proj, (100-alpha)/100);
+ // центральная точка перспективной проекции
+ if (show) centerPoint(canvas);
+}
+// центральная точка перспективной проекции
+function centerPoint(canvas) {
+ const context = canvas.getContext('2d');
+ context.beginPath();
+ context.lineWidth = 3.2;
+ context.strokeStyle = '#66bb6a';
+ context.arc(tv5.x, tv5.y, 6.5, 0, 2*Math.PI);
+ context.stroke();
+}
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint5,50));
diff --git a/jekyll_site/js/spinning-square.js b/jekyll_site/js/spinning-square.js
new file mode 100644
index 0000000..6034980
--- /dev/null
+++ b/jekyll_site/js/spinning-square.js
@@ -0,0 +1,52 @@
+// © Головин Г.Г., Код с комментариями, 2023
+'use strict';
+let canvas = document.getElementById('canvas');
+// исходный массив точек-вершин квадрата
+let square = [{x:50,y:50},{x:50,y:250},{x:250,y:250},{x:250,y:50}];
+// центр фигуры, вокруг него будем выполнять поворот
+let t0 = {x:150, y:150};
+// угол поворота в градусах
+let deg = 1;
+
+// поворот фигуры и обновление изображения
+function repaint() {
+ // поворачиваем исходный массив точек на угол
+ for (let i = 0; i < square.length; i++)
+ square[i] = rotateOnDegree(t0, square[i], deg);
+ // рисуем текущий массив точек
+ drawFigure(canvas, square);
+}
+
+// поворачиваем точку (t) на угол (deg) относительно точки (t0)
+function rotateOnDegree(t0, t, deg) {
+ let t_new = {};
+ // переводим угол поворота из градусов в радианы
+ let rad = (Math.PI / 180) * deg;
+ // рассчитываем координаты новой точки по формуле
+ t_new.x = t0.x+(t.x-t0.x)*Math.cos(rad)-(t.y-t0.y)*Math.sin(rad);
+ t_new.y = t0.y+(t.x-t0.x)*Math.sin(rad)+(t.y-t0.y)*Math.cos(rad);
+ // возвращаем новую точку
+ return t_new;
+}
+
+// рисуем фигуру по точкам из массива
+function drawFigure(canvas, arr) {
+ let context = canvas.getContext('2d');
+ // очищаем весь холст целиком
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // обходим массив точек и соединяем их линиями
+ context.beginPath();
+ for (let i = 0; i < arr.length; i++)
+ if (i == 0)
+ context.moveTo(arr[i].x, arr[i].y);
+ else
+ context.lineTo(arr[i].x, arr[i].y);
+ context.closePath();
+ // рисуем линии на холсте
+ context.lineWidth = 2.2;
+ context.strokeStyle = '#222';
+ context.stroke();
+}
+
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
diff --git a/jekyll_site/js/spinning-square2.js b/jekyll_site/js/spinning-square2.js
new file mode 100644
index 0000000..94b3c82
--- /dev/null
+++ b/jekyll_site/js/spinning-square2.js
@@ -0,0 +1,26 @@
+// © Головин Г.Г., Код с комментариями, 2023
+'use strict';
+let canvas2 = document.getElementById('canvas2');
+// текущий массив точек
+let square2 = [];
+// вращающаяся точка
+let t2 = {x:100, y:100};
+
+// поворот фигуры и обновление изображения
+function repaint2() {
+ // поворачиваем точку в обратную сторону
+ t2 = rotateOnDegree(t0, t2, -deg);
+ // обходим точки исходного массива и сдвигаем
+ for (let i = 0; i < square.length; i++) {
+ // текущая точка
+ square2[i] = {};
+ // сдвигаем точку исходного массива
+ square2[i].x = square[i].x - t0.x + t2.x;
+ square2[i].y = square[i].y - t0.y + t2.y;
+ }
+ // рисуем текущий массив точек
+ drawFigure(canvas2, square2);
+}
+
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint2,50));
diff --git a/jekyll_site/js/tetris-controller.js b/jekyll_site/js/tetris-controller.js
new file mode 100644
index 0000000..802bb3c
--- /dev/null
+++ b/jekyll_site/js/tetris-controller.js
@@ -0,0 +1,177 @@
+// © Головин Г.Г., Обработка действий пользователя, 2023
+'use strict';
+// коды кнопок на клавиатуре
+const KEY = {PAUSE:19,SPACE:32,LEFT:37,UP:38,RIGHT:39,DOWN:40};
+// типы объёмного изображения
+const VOLUME = {FLAT:0,PARALLEL:1,PERSPECTIVE:2};
+// текущий тип изображения
+let vol = VOLUME.FLAT;
+// кнопка нажата и удерживается
+function keyPressed(caller) {
+ if (status == GAME.OVER) return;
+ switch (caller.keyCode) {
+ case KEY.PAUSE: {
+ status = GAME.PAUSE;
+ statusView.refresh();
+ return;
+ }
+ case KEY.LEFT: {
+ caller.preventDefault();
+ moveFigureLeft();
+ break;
+ }
+ case KEY.RIGHT: {
+ caller.preventDefault();
+ moveFigureRight();
+ break;
+ }
+ case KEY.SPACE:
+ case KEY.UP: {
+ caller.preventDefault();
+ rotateFigure();
+ break;
+ }
+ case KEY.DOWN: {
+ caller.preventDefault();
+ rapidFall = true;
+ if (sleepTimeout != undefined) {
+ clearTimeout(sleepTimeout);
+ stepDown();
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ if (status == GAME.LEVEL || status == GAME.PAUSE) {
+ status = GAME.RUN;
+ setTimeout(figureFall, 60);
+ }
+}
+// кнопка отпущена
+function keyReleased(caller) {
+ if (caller.keyCode == KEY.DOWN) {
+ rapidFall = false;
+ }
+}
+// изменить количество строк
+function changeRows(caller) {
+ rows = caller.target.valueAsNumber;
+ container.changeSize();
+ t0.reCalc();
+ tv2.reCalc();
+ refreshParams();
+ prepareNewGame();
+}
+// изменить количество колонок
+function changeColumns(caller) {
+ columns = caller.target.valueAsNumber;
+ container.changeSize();
+ t0.reCalc();
+ tv2.reCalc();
+ refreshParams();
+ prepareNewGame();
+}
+// переключить режим улитки
+function changeSnailMode(caller) {
+ if (caller.target.checked)
+ reduction=REDUCTION_SNAIL;
+ else
+ reduction=REDUCTION_STEP;
+}
+// изменить тип объёмного изображения
+function changeVolume(caller) {
+ const value = caller.target.value;
+ const old = vol;
+ if (value=="off") vol=VOLUME.FLAT;
+ if (value=="parallel") vol=VOLUME.PARALLEL;
+ if (value=="perspective") vol=VOLUME.PERSPECTIVE;
+ refreshDisabled();
+ repaint(false);
+}
+// изменяем прозрачность фигур
+function changeAlpha(caller) {
+ colors.alpha=caller.target.valueAsNumber;
+}
+// поворачиваем кубики, изменяем текущий угол
+function rotate(axis, caller) {
+ deg[axis] = caller.target.valueAsNumber;
+ prepare3D();
+}
+// вертикальная корректировка
+function changeTv1(caller) {
+ tv1.y = caller.target.valueAsNumber;
+}
+// центральная точка на экране наблюдателя
+function changeTv2(axis, caller) {
+ tv2[axis] = caller.target.valueAsNumber;
+}
+// удалённость источника проекции
+function changeDistance2(caller) {
+ d2=caller.target.valueAsNumber;
+}
+// показать центральную точку
+function showCenter(caller) {
+ tv2.show=!tv2.show;
+}
+// обновить игру и все настройки
+function reload() {
+ vol=VOLUME.FLAT;
+ field3D=[];
+ colors.setDefault();
+ deg.setDefault();
+ tv1.reCalc();
+ tv2.reCalc();
+ reduction=REDUCTION_STEP;
+ prepareNewGame();
+ refreshParams();
+}
+// обновить отображение параметров
+function refreshParams() {
+ document.getElementById('speedometer').value=0;
+ document.getElementById('snail').checked=(reduction==REDUCTION_SNAIL);
+ document.getElementById('alpha').value=colors.alpha;
+ document.getElementById('oAlpha').value=colors.alpha + '%';
+ document.getElementById('off').checked=(vol==VOLUME.FLAT);
+ document.getElementById('parallel').checked=(vol==VOLUME.PARALLEL);
+ document.getElementById('perspective').checked=(vol==VOLUME.PERSPECTIVE);
+ document.getElementById('rotateX').value = deg.x;
+ document.getElementById('rotateXo').value = deg.x + '°';
+ document.getElementById('rotateY').value = deg.y;
+ document.getElementById('rotateYo').value = deg.y + '°';
+ document.getElementById('rotateZ').value = deg.z;
+ document.getElementById('rotateZo').value = deg.z + '°';
+ document.getElementById('tv1Y').value = tv1.y;
+ document.getElementById('tv1Yo').value = tv1.y;
+ document.getElementById('center').checked = tv2.show;
+ document.getElementById('tv2X').max = container.canvas.width;
+ document.getElementById('tv2X').value = tv2.x;
+ document.getElementById('tv2Xo').value = tv2.x;
+ document.getElementById('tv2Y').max = container.canvas.height;
+ document.getElementById('tv2Y').value = tv2.y;
+ document.getElementById('tv2Yo').value = tv2.y;
+ document.getElementById('tv2Z').max = d2/2;
+ document.getElementById('tv2Z').value = tv2.z;
+ document.getElementById('tv2Zo').value = tv2.z;
+ document.getElementById('dist').max = d2*2;
+ document.getElementById('dist').value = d2;
+ document.getElementById('oDist').value = d2;
+ refreshDisabled();
+}
+// обновить доступность блоков
+function refreshDisabled() {
+ document.getElementById('rotateX').disabled=(vol==VOLUME.FLAT);
+ document.getElementById('rotateY').disabled=(vol==VOLUME.FLAT);
+ document.getElementById('rotateZ').disabled=(vol==VOLUME.FLAT);
+ document.getElementById('tv1Y').disabled=(vol!=VOLUME.PARALLEL);
+ document.getElementById('center').disabled=(vol!=VOLUME.PERSPECTIVE);
+ document.getElementById('tv2X').disabled=(vol!=VOLUME.PERSPECTIVE);
+ document.getElementById('tv2Y').disabled=(vol!=VOLUME.PERSPECTIVE);
+ document.getElementById('tv2Z').disabled=(vol!=VOLUME.PERSPECTIVE);
+ document.getElementById('dist').disabled=(vol!=VOLUME.PERSPECTIVE);
+}
+// после загрузки всех частей страницы
+document.addEventListener('DOMContentLoaded', function() {
+ refreshParams();
+});
diff --git a/jekyll_site/js/tetris-figures.js b/jekyll_site/js/tetris-figures.js
new file mode 100644
index 0000000..17a873c
--- /dev/null
+++ b/jekyll_site/js/tetris-figures.js
@@ -0,0 +1,42 @@
+// © Головин Г.Г., Набор фигур, 2023
+'use strict';
+// фигуры тетрамино
+const FIGURE=[
+ [[1,1],[1,0],[1,0]],
+ [[2,2],[0,2],[0,2]],
+ [[0,3],[3,3],[3,0]],
+ [[4,0],[4,4],[0,4]],
+ [[5,5],[5,5]],
+ [[6],[6],[6],[6]],
+ [[7,0],[7,7],[7,0]]];
+// полный набор фигур
+FIGURE.set = function() {
+ let set = [];
+ for (let i=0; i6) return undefined;
+ this.type=num+1;
+ this.shape=FIGURE[num];
+ }
+ // копия текущего объекта
+ clone() {
+ return new Figure(this.type-1);
+ }
+ // поворот по часовой стрелке
+ rotate() {
+ let nShape = [], shape = this.shape;
+ for (let y = 0; y < shape.length; y++)
+ for (let x = 0; x < shape[y].length; x++) {
+ if (nShape[x]==undefined) nShape[x] = [];
+ nShape[x][shape.length-y-1] = shape[y][x];
+ }
+ this.shape=nShape;
+ }
+};
diff --git a/jekyll_site/js/tetris-model.js b/jekyll_site/js/tetris-model.js
new file mode 100644
index 0000000..e0359ec
--- /dev/null
+++ b/jekyll_site/js/tetris-model.js
@@ -0,0 +1,245 @@
+// © Головин Г.Г., Логика игрового процесса, 2023
+'use strict';
+// игровое поле и его размеры
+let field, rows = 20, columns = 10;
+// ускорение падения фигур
+const REDUCTION_STEP = 25;
+const REDUCTION_SNAIL = 5;
+let reduction = REDUCTION_STEP;
+// скорость падения фигур
+const START_DELAY = 600;
+const MIN_DELAY = 80;
+let stepDelay = START_DELAY;
+let sleepTimeout, rapidFall = false;
+// уровень, следующий уровень, счёт
+let level, nextLevel, score;
+// массив фигур тетрамино
+const figures = FIGURE.set();
+// текущая фигура, следующая фигура
+let currentFigure, nextFigure;
+// статусы игры
+const GAME = {RUN:0,LEVEL:1,PAUSE:2,OVER:3};
+// текущий статус
+let status = GAME.PAUSE;
+// подготовить новую игру
+function prepareNewGame() {
+ field = [];
+ for (let i = 0; i < rows; i++) {
+ field[i] = [];
+ for (let j = 0; j < columns; j++)
+ field[i][j] = 0;
+ }
+ status = GAME.PAUSE;
+ level = 0;
+ score = 0;
+ nextLevel = (10 * columns) * (level + 1);
+ stepDelay = START_DELAY;
+ startFigureFall();
+ repaint();
+}
+// начало падения фигуры
+function startFigureFall() {
+ currentFigure = nextFigure;
+ const x = Math.floor(columns/2+columns%2-1);
+ if (isFreeSpace(0, x)) {
+ doPlaceFigure(0, x);
+ repaint();
+ } else {
+ status=GAME.OVER;
+ for (let y=-1; y>-4; y--)
+ if (isFreeSpace(y, x, false)) {
+ doPlaceFigure(y, x);
+ break;
+ }
+ repaint();
+ return;
+ }
+ const rnd = Math.floor(Math.random() * figures.length);
+ nextFigure = figures[rnd].clone();
+ if (status==GAME.LEVEL) return;
+ setTimeout(figureFall, 60);
+}
+// падение фигуры
+function figureFall() {
+ if (rapidFall)
+ stepDown();
+ else
+ sleepTimeout = setTimeout(stepDown, stepDelay);
+}
+// шаг вниз
+function stepDown() {
+ sleepTimeout = undefined;
+ if (status==GAME.PAUSE) return;
+ if (isSpaceDown()) {
+ doStepDown();
+ setTimeout(figureFall, 60);
+ } else {
+ mergeFigure(currentFigure.type);
+ let fullRows = 0;
+ for (let i = 0; i < rows; i++)
+ if (isFullRow(i)) {
+ for (let c = 3; c >= 0; c--)
+ setTimeout(blinkFullRow, 400 * fullRows + 100 * c, i, c);
+ setTimeout(slideDown, 400 * fullRows + 400, i);
+ fullRows++;
+ }
+ score += columns * [0,1,3,5,10][fullRows];
+ if (score >= nextLevel) {
+ level++;
+ status = GAME.LEVEL;
+ nextLevel = (10 * columns) * (level + 1);
+ stepDelay = Math.max(MIN_DELAY,stepDelay-reduction);
+ }
+ startFigureFall();
+ }
+}
+// свободное место ниже текущей фигуры
+function isSpaceDown() {
+ for (let y = rows-2; y >= 0; y--)
+ for (let x = 0; x < columns; x++)
+ if (field[y][x]==11 && field[y+1][x]>0 && field[y+1][x]<11
+ || field[y+1][x]==11 && y==rows-2)
+ return false;
+ return true;
+}
+// сдвинуть текущую фигуру вниз
+function doStepDown() {
+ for (let y = rows-2; y >= 0; y--)
+ for (let x = 0; x < columns; x++)
+ if (field[y][x]==11) {
+ field[y][x]=0;
+ field[y+1][x]=11;
+ }
+ repaint();
+}
+// завершение движения текущей фигуры
+function mergeFigure(type) {
+ for (let x = 0; x < columns; x++)
+ for (let y = 0; y < rows; y++)
+ if (field[y][x] == 11)
+ field[y][x] = type;
+}
+// заполненная строка
+function isFullRow(row) {
+ for (let x = 0; x < columns; x++)
+ if (field[row][x] == 0)
+ return false;
+ return true;
+}
+// моргание заполненной строки
+function blinkFullRow(row, color) {
+ for (let x = 0; x < columns; x++)
+ field[row][x] = color;
+ repaint();
+}
+// сдвинуть поле вниз
+function slideDown(row) {
+ for (let y = row-1; y >= 0; y--)
+ for (let x = 0; x < columns; x++)
+ if (field[y+1][x]!=11 && field[y][x]!=11)
+ field[y+1][x]=field[y][x];
+ repaint();
+}
+// свободное место для текущей фигуры в пределах границ поля
+function isFreeSpace(y, x, fullSize=true) {
+ const height = currentFigure.shape.length;
+ const wight = currentFigure.shape[0].length;
+ if (fullSize && (y<0 || y+height>rows || x<0 || x+wight>columns))
+ return false;
+ for (let yy=0; yy0)
+ if (y+yy>=0 && y+yy=0 && x+xx0)
+ if (y+yy>=0 && y+yy=0 && x+xx0 && field[y][x-1]<11
+ || field[y][x-1]==11 && x==1)
+ return false;
+ return true;
+}
+// сдвинуть текущую фигуру влево
+function doStepLeft() {
+ for (let x = 1; x < columns; x++)
+ for (let y = 0; y < rows; y++)
+ if (field[y][x]==11) {
+ field[y][x]=0;
+ field[y][x-1]=11;
+ }
+}
+// для вызова из контроллера
+function moveFigureRight() {
+ if (isSpaceRight())
+ doStepRight();
+ repaint();
+}
+// свободное место справа от текущей фигуры
+function isSpaceRight() {
+ for (let x = columns-2; x >=0; x--)
+ for (let y = 0; y < rows; y++)
+ if (field[y][x]==11 && field[y][x+1]>0 && field[y][x+1]<11
+ || field[y][x+1]==11 && x==columns-2)
+ return false;
+ return true;
+}
+// сдвинуть текущую фигуру вправо
+function doStepRight() {
+ for (let x = columns-2; x >=0; x--)
+ for (let y = 0; y < rows; y++)
+ if (field[y][x]==11) {
+ field[y][x]=0;
+ field[y][x+1]=11;
+ }
+}
+// для вызова из контроллера, поворот фигуры
+function rotateFigure() {
+ let y = rows, x = columns;
+ for (let yy = 0; yy < rows; yy++)
+ for (let xx = 0; xx < columns; xx++)
+ if (field[yy][xx] == 11) {
+ if (y > yy) y = yy;
+ if (x > xx) x = xx;
+ }
+ if (y == rows || x == columns) return;
+ const old = currentFigure.shape;
+ doPlaceFigure(y, x, 0);
+ currentFigure.rotate();
+ if (isFreeSpace(y, x))
+ doPlaceFigure(y, x);
+ else if (isFreeSpace(y, x-1))
+ doPlaceFigure(y, x-1);
+ else {
+ currentFigure.shape = old;
+ doPlaceFigure(y, x);
+ }
+ repaint();
+}
+// после загрузки всех частей страницы, запускаем игру
+document.addEventListener('DOMContentLoaded', function() {
+ addEventListener('keydown', keyPressed);
+ addEventListener('keyup', keyReleased);
+ const rnd = Math.floor(Math.random() * figures.length);
+ nextFigure = figures[rnd].clone();
+ prepareNewGame();
+});
diff --git a/jekyll_site/js/tetris-view.js b/jekyll_site/js/tetris-view.js
new file mode 100644
index 0000000..f0181d6
--- /dev/null
+++ b/jekyll_site/js/tetris-view.js
@@ -0,0 +1,235 @@
+// © Головин Г.Г., Визуализация игрового процесса, 2023
+'use strict';
+// размер клеточки, отступ
+const size = 30, gap = 2;
+// массив статусов игры
+const statuses = ["ИГРА","УРОВЕНЬ","ПАУЗА","ЗАВЕРШЕНО"];
+// массив цветов для фигур
+const colors = [
+ 'rgba(240,240,240,1)',
+ 'rgba(150,0,0,1)',
+ 'rgba(0,0,150,1)',
+ 'rgba(150,150,0,1)',
+ 'rgba(0,150,150,1)',
+ 'rgba(0,150,0,1)',
+ 'rgba(150,0,150,1)',
+ 'rgba(150,80,80,1)'];
+// прозрачность фигур
+colors.setDefault = function() {
+ this.alpha = 0;
+}
+colors.setDefault();
+// получаем цвет, добавляем прозрачность только фигурам
+colors.get = function(...numbers) {
+ for (let num of numbers)
+ if (num <= 0) return this[0];
+ else if (num < colors.length)
+ return this[num].replace('1)', (100-this.alpha)/100 + ')');
+ return this[0];
+}
+// массив для кубиков
+let field3D = [];
+// центральная точка для поворотов
+const t0 = {}; t0.reCalc = function() {
+ this.x = columns*(size+gap)/2;
+ this.y = rows*(size+gap);
+ this.z = (size+gap)*2;
+};
+t0.reCalc();
+// угол поворота игрового поля с кубиками
+let deg={}; deg.setDefault = function() {
+ this.x=-1;
+ this.y=0;
+ this.z=0;
+};
+deg.setDefault();
+// параллельная проекция: центр и экран наблюдателя
+let d1 = 600, tv1={}; tv1.reCalc = function() {
+ this.x=columns*(size+gap)/2;
+ this.y=(size+gap)*2;
+ this.z=(size+gap)*2;
+ d1 = Math.max(rows,columns)*(size+gap);
+}
+tv1.reCalc();
+// перспективная проекция: центр и экран наблюдателя
+let d2 = 600, tv2 = {}; tv2.reCalc = function() {
+ this.x = columns*(size+gap)/2;
+ this.y = rows*(size+gap)/2;
+ this.z = (size+gap)*2;
+ this.show=false;
+ d2 = Math.max(rows,columns)*(size+gap);
+};
+tv2.reCalc();
+// стакан с игрой
+const container = {};
+container.canvas = document.getElementById('container');
+container.context = container.canvas.getContext('2d');
+container.changeSize = function() {
+ this.canvas.width = columns*size+(columns+1)*gap;
+ this.canvas.height = rows*size+(rows+1)*gap;
+};
+container.changeSize();
+// перетаскивание центральной точки мышью
+container.msBtnPressed = false;
+container.canvas.onmouseup = ()=> container.msBtnPressed=false;
+container.canvas.onmousedown = (caller)=> {
+ container.msBtnPressed=true;
+ container.canvas.onmousemove(caller);
+}
+container.canvas.onmousemove = (caller)=> {
+ if (vol==VOLUME.PERSPECTIVE && container.msBtnPressed && tv2.show) {
+ tv2.x = caller.offsetX;
+ tv2.y = caller.offsetY;
+ repaint(false);
+ refreshParams();
+ }
+}
+// следующая фигура
+const next = {};
+next.canvas = document.getElementById('next');
+next.context = next.canvas.getContext('2d');
+next.changeSize = function() {
+ this.canvas.width = 2*size+3*gap;
+ this.canvas.height = 4*size+5*gap;
+};
+next.changeSize();
+// состояние игры
+const statusView = {};
+statusView.level = document.getElementById('levelView');
+statusView.score = document.getElementById('scoreView');
+statusView.nextLevel = document.getElementById('nextLevelView');
+statusView.speed = document.getElementById('speedometer')
+statusView.status = document.getElementById('statusView');
+statusView.refresh = function() {
+ this.level.innerText = level;
+ this.score.innerText = score;
+ this.nextLevel.innerText = nextLevel;
+ this.speed.value = speedometer();
+ this.status.innerText = statuses[status];
+}
+// обновление изображения
+function repaint(refresh3D=true) {
+ // состояние игры
+ statusView.refresh();
+ // стакан с игрой
+ if (vol==VOLUME.FLAT) {
+ drawCells(container.canvas, container.context, field, currentFigure.type);
+ } else {
+ if (refresh3D || field3D.length==0)
+ prepare3D();
+ repaint3D();
+ }
+ // следующая фигура
+ drawCells(next.canvas, next.context, nextFigure.shape);
+}
+// рисуем массив клеточек
+function drawCells(canvas, context, array, color) {
+ // очищаем весь холст целиком
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // обходим массив, рисуем клеточки
+ for (let y=0; y b.dist-a.dist);
+ // удаляем смежные стенки между соседними кубиками
+ for (let i=0, j=1; iMath.abs(b.dist-a.dist)>size ? b.dist-a.dist : b.clock-a.clock);
+ // обновляем стакан с игрой
+ drawCubes(container.canvas, container.context, proj);
+ // центральная точка перспективной проекции
+ if (vol==VOLUME.PERSPECTIVE && tv2.show) centerPoint(container.context);
+}
+// обходим массив, рисуем грани по точкам
+function drawCubes(canvas, context, array) {
+ // очищаем весь холст целиком
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // рисуем только видимые грани
+ const visible = function(face) {
+ // если есть хотя бы одна точка, которую можно нарисовать
+ for (let j = 0; j < face.length; j++)
+ if (face[j].x>-size && face[j].x-size && face[j].y
+
+
+
+### HTML {#html1}
+
+```html
+
+```
+
+### JavaScript {#javascript1}
+
+```js
+'use strict';
+let canvas = document.getElementById('canvas');
+// исходный массив точек-вершин квадрата
+let square = [{x:50,y:50},{x:50,y:250},{x:250,y:250},{x:250,y:50}];
+// центр фигуры, вокруг него будем выполнять поворот
+let t0 = {x:150, y:150};
+// угол поворота в градусах
+let deg = 1;
+```
+```js
+// поворот фигуры и обновление изображения
+function repaint() {
+ // поворачиваем исходный массив точек на угол
+ for (let i = 0; i < square.length; i++)
+ square[i] = rotateOnDegree(t0, square[i], deg);
+ // рисуем текущий массив точек
+ drawFigure(canvas, square);
+}
+```
+```js
+// поворачиваем точку (t) на угол (deg) относительно точки (t0)
+function rotateOnDegree(t0, t, deg) {
+ let t_new = {};
+ // переводим угол поворота из градусов в радианы
+ let rad = (Math.PI / 180) * deg;
+ // рассчитываем координаты новой точки по формуле
+ t_new.x = t0.x+(t.x-t0.x)*Math.cos(rad)-(t.y-t0.y)*Math.sin(rad);
+ t_new.y = t0.y+(t.x-t0.x)*Math.sin(rad)+(t.y-t0.y)*Math.cos(rad);
+ // возвращаем новую точку
+ return t_new;
+}
+```
+```js
+// рисуем фигуру по точкам из массива
+function drawFigure(canvas, arr) {
+ let context = canvas.getContext('2d');
+ // очищаем весь холст целиком
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // обходим массив точек и соединяем их линиями
+ context.beginPath();
+ for (let i = 0; i < arr.length; i++)
+ if (i == 0)
+ context.moveTo(arr[i].x, arr[i].y);
+ else
+ context.lineTo(arr[i].x, arr[i].y);
+ context.closePath();
+ // рисуем линии на холсте
+ context.lineWidth = 2.2;
+ context.strokeStyle = '#222';
+ context.stroke();
+}
+```
+```js
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
+```
+
+## Вращение в обратную сторону {#spinning-backwards}
+
+Добавим ещё одну точку, которую будем вращать в обратную сторону. Точка удалена от центра фигуры на
+четверть длины стороны квадрата. Сместим центр квадрата в эту точку — сдвинем массив его вершин. Сам
+квадрат будем вращать по часовой стрелке, а его центральную точку — против часовой стрелки. Этот код
+работает вместе с предыдущим.
+
+
+
+
+
+### HTML {#html2}
+
+```html
+
+```
+
+### JavaScript {#javascript2}
+
+```js
+'use strict';
+let canvas2 = document.getElementById('canvas2');
+// текущий массив точек
+let square2 = [];
+// вращающаяся точка
+let t2 = {x:100, y:100};
+```
+```js
+// поворот фигуры и обновление изображения
+function repaint2() {
+ // поворачиваем точку в обратную сторону
+ t2 = rotateOnDegree(t0, t2, -deg);
+ // обходим точки исходного массива и сдвигаем
+ for (let i = 0; i < square.length; i++) {
+ // текущая точка
+ square2[i] = {};
+ // сдвигаем точку исходного массива
+ square2[i].x = square[i].x - t0.x + t2.x;
+ square2[i].y = square[i].y - t0.y + t2.y;
+ }
+ // рисуем текущий массив точек
+ drawFigure(canvas2, square2);
+}
+```
+```js
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint2,50));
+```
diff --git a/jekyll_site/ru/2023/01/10/spinning-cube-in-space.md b/jekyll_site/ru/2023/01/10/spinning-cube-in-space.md
new file mode 100644
index 0000000..c1abea9
--- /dev/null
+++ b/jekyll_site/ru/2023/01/10/spinning-cube-in-space.md
@@ -0,0 +1,226 @@
+---
+title: Вращаем куб в пространстве
+description: Рассматриваем разницу между параллельной и перспективной проекцией. Обе широко используются на практике для различных целей. В предыдущем примере мы вращали...
+sections: [Линейная перспектива,Матрица поворота,Экспериментальная модель]
+tags: [javascript,canvas,геометрия,графика,изображение,картинка,квадрат,куб]
+scripts: [/js/classes-point-cube.js,/js/spinning-cube.js,/js/spinning-cube2.js]
+styles: [/css/pomodoro1.css]
+canonical_url: /ru/2023/01/10/spinning-cube-in-space.html
+url_translated: /en/2023/01/11/spinning-cube-in-space.html
+title_translated: Spinning cube in space
+date: 2023.01.10
+---
+
+Рассматриваем разницу между параллельной и перспективной проекцией.
+Обе широко используются на практике для различных целей. В предыдущем примере мы
+[вращали квадрат на плоскости]({{ '/ru/2023/01/05/spinning-square-on-plane.html' | relative_url }})
+— переходим в трёхмерное пространство. Теперь, чтобы отобразить на плоскости экрана поворот трёхмерного
+объекта, нужно сначала создать трёхмерный объект, повернуть его на угол, срисовать с него проекцию и
+отобразить на экране уже проекцию.
+
+Усложнённая модель, много кубиков: [Вращаем пространственный крест]({{ '/ru/2023/01/15/spinning-spatial-cross.html' | relative_url }}).
+
+
+
+ Параллельная проекция
+
+
+
+ Перспективная проекция
+
+
+
+
+*Параллельная проекция* — центр проекции бесконечно удалён от плоскости экрана наблюдателя,
+размеры предметов выглядят одинаковыми.
+
+*Перспективная проекция* — параллельные линии сходятся в центре перспективы, предметы выглядят
+уменьшающимися вдалеке.
+
+## Экспериментальная модель {#experimental-model}
+
+Размер куба 200, размер холста 300, начало координат находится в верхнем левом углу. Центр фигуры
+в середине холста. Ось `X` направлена вправо, ось `Y` направлена вниз, ось `Z` направлена вдаль.
+Выполняется поворот последовательно по всем трём осям: сначала по оси `X`, затем по оси `Y` и затем
+по оси `Z`. Настройками модели можно управлять, например можно отключать лишнее вращение по осям и
+изменять положение центра проекции на экране наблюдателя.
+
+
+
+
+
+
+## Поворот точки в пространстве {#point-rotation-in-space}
+
+Рассчитываем новые координаты точки по формулам матрицы поворота для трёхмерного пространства.
+Поворачиваем точку `t` относительно точки `t0` — получаем точку `t'`.
+
+*Поворот по оси `X`.*
+
+{% include image_svg.html src="/img/column-vector3dx.svg" style="width: 242.619pt; height: 59.0768pt;"
+alt="&x'=x,&\\&y'=y_0+(y-y_0)cos\varphi-(z-z_0)sin\varphi,&\\&z'=z_0+(y-y_0)sin\varphi+(z-z_0)cos\varphi.&\\" %}
+
+*Поворот по оси `Y`.*
+
+{% include image_svg.html src="/img/column-vector3dy.svg" style="width: 246.251pt; height: 59.0768pt;"
+alt="&x'=x_0+(x-x_0)cos\varphi-(z-z_0)sin\varphi,&\\&y'=y,&\\&z'=z_0+(x-x_0)sin\varphi+(z-z_0)cos\varphi.&\\" %}
+
+*Поворот по оси `Z`.*
+
+{% include image_svg.html src="/img/column-vector3dz.svg" style="width: 246.793pt; height: 55.4753pt;"
+alt="&x'=x_0+(x-x_0)cos\varphi-(y-y_0)sin\varphi,&\\&y'=y_0+(x-x_0)sin\varphi+(y-y_0)cos\varphi,&\\&z'=z.&\\" %}
+
+## Проекция точки {#point-projection}
+
+Экспериментальные формулы с возможностью смещения центра проекции `d0` на экране наблюдателя `tv`.
+Отображаем точку пространства `t` на плоскость экрана — получаем точку `t'`.
+
+*Параллельная проекция.*
+
+{% include image_svg.html src="/img/oblique-projection.svg" style="width: 123.97pt; height: 37.2836pt;"
+alt="&x'=x,&\\&y'=y+(y_v-z)/4.&\\" %}
+
+*Перспективная проекция.*
+
+{% include image_svg.html src="/img/central-projection.svg" style="width: 231.924pt; height: 37.2836pt;"
+alt="&x'=x_v+d_0\cdot(x-x_v)/(z-z_v+d_0),&\\&y'=y_v+d_0\cdot(y-y_v)/(z-z_v+d_0).&\\" %}
+
+*Расстояние от точки до центра проекции.*
+
+{% include image_svg.html src="/img/euclidean-distance.svg" style="width: 319.911pt; height: 17.9328pt;"
+alt="d(t,d_0)=\sqrt{(x-x_v)^2+(y-y_v)^2+(z-z_v+d_0)^2}." %}
+
+## Сортировка граней {#face-sorting}
+
+При создании кубика, вершины каждой грани задаём по часовой стрелке. При получении проекции, подставляем
+в уравнение прямой три подряд идущие вершины, чтобы определить наклон грани и удалённость её от плоскости
+проекции.
+
+*Уравнение прямой, проходящей через две точки.*
+
+{% include image_svg.html src="/img/linear-equation.svg" style="width: 137.171pt; height: 35.3194pt;"
+alt="{(x-x_1)\over(y-y_1)}={(x_2-x_1)\over(y_2-y_1)}." %}
+
+## Описание алгоритма {#algorithm-description}
+
+Сначала обходим вершины куба и поворачиваем их на угол относительно центральной точки. Затем обходим грани
+куба и получаем проекции входящих в них вершин. После этого сортируем проекции граней по удалённости. Затем
+рисуем проекции на плоскости — соединяем точки линиями. Рисуем полупрозрачным цветом сперва дальние грани и
+поверх них ближние, чтобы сквозь ближние грани было видно дальние.
+
+На каждом шаге отображения фигуры повторяем сортировку граней по удалённости, так как с изменением
+угла поворота, координаты смещаются, и ближние грани становятся дальними.
+
+## Реализация на JavaScript {#implementation-in-javascript}
+
+{% include classes-point-cube-ru.md -%}
+
+Создаём объект и рисуем две проекции на плоскости.
+
+```js
+'use strict';
+// рисовать будем сразу две картинки,
+// объект будет один, а проекций будет много
+const canvas1 = document.getElementById('canvas1');
+const canvas2 = document.getElementById('canvas2');
+// создаём объект
+const cube = new Cube(50,50,50,200);
+// центр фигуры, вокруг него будем выполнять поворот
+const t0 = new Point(150,150,150);
+// удалённость центра проекции
+const d = 300;
+// положение экрана наблюдателя
+const tv = new Point(150,150,80);
+// угол поворота в градусах
+const deg = {x:0,y:1,z:0};
+```
+```js
+// поворот фигуры и обновление изображения
+function repaint() {
+ cube.rotate(deg, t0);
+ // рисуем параллельную проекцию
+ drawFigure(canvas1, cube.projection('parallel', tv));
+ // рисуем перспективную проекцию
+ drawFigure(canvas2, cube.projection('perspective', tv, d));
+}
+```
+```js
+// рисуем фигуру по точкам из массива
+function drawFigure(canvas, proj) {
+ let context = canvas.getContext('2d');
+ // сортируем грани по их наклону
+ proj.sort((a,b) => b.clock-a.clock);
+ // очищаем весь холст целиком
+ context.clearRect(0, 0, canvas.width, canvas.height);
+ // обходим массив граней куба
+ for (let i = 0; i < proj.length; i++) {
+ // обходим массив точек и соединяем их линиями
+ context.beginPath();
+ for (let j = 0; j < proj[i].length; j++) {
+ if (j == 0) {
+ context.moveTo(proj[i][j].x, proj[i][j].y);
+ } else {
+ context.lineTo(proj[i][j].x, proj[i][j].y);
+ }
+ }
+ context.closePath();
+ // рисуем грань куба вместе с рёбрами
+ context.lineWidth = 2.2;
+ context.lineJoin = 'round';
+ context.fillStyle = '#fff9';
+ context.strokeStyle = '#222';
+ context.fill();
+ context.stroke();
+ }
+}
+```
+```js
+// после загрузки страницы, задаём интервал обновления изображения
+document.addEventListener('DOMContentLoaded',()=>setInterval(repaint,50));
+```
diff --git a/jekyll_site/ru/2023/01/15/spinning-spatial-cross.md b/jekyll_site/ru/2023/01/15/spinning-spatial-cross.md
new file mode 100644
index 0000000..4eda438
--- /dev/null
+++ b/jekyll_site/ru/2023/01/15/spinning-spatial-cross.md
@@ -0,0 +1,271 @@
+---
+title: Вращаем пространственный крест
+description: Пишем алгоритм для поворота объёмной фигуры на угол вокруг своего центра по всем трём осям сразу. В предыдущем примере мы вращали куб в пространстве...
+sections: [Объёмные фигуры,Матрица поворота,Экспериментальная модель]
+tags: [javascript,canvas,геометрия,матрица,графика,изображение,картинка,квадрат,куб]
+scripts: [/js/classes-point-cube.js,/js/spinning-spatial-cross.js,/js/spinning-spatial-cross2.js]
+styles: [/css/pomodoro1.css]
+canonical_url: /ru/2023/01/15/spinning-spatial-cross.html
+url_translated: /en/2023/01/16/spinning-spatial-cross.html
+title_translated: Spinning spatial cross
+date: 2023.01.15
+---
+
+Пишем алгоритм для поворота объёмной фигуры на угол вокруг своего центра по всем трём осям сразу. В предыдущем
+примере мы [вращали куб в пространстве]({{ '/ru/2023/01/10/spinning-cube-in-space.html' | relative_url }})
+— теперь кубиков будет много, алгоритм будет почти такой же и формулы будем использовать те же. Рисуем два
+варианта фигуры: *пространственный крест* и *крест-куб* в двух типах проекций, рассматриваем разницу.
+
+Тестирование экспериментального интерфейса: [Объёмный тетрис]({{ '/ru/2023/01/21/volumetric-tetris.html' | relative_url }}).
+
+## Пространственный крест {#spatial-cross}
+
+