- PVSM.RU - https://www.pvsm.ru -
У Радислава Гандапаса есть отличная книга Полная Ж [1]. В ней говорится о том, как оценить направления своей жизни, и как разработать план развития.
Мне захотелось создать инструмент, который будет в моем смартфоне и поможет составить мой радар.
Исходный код туториала и демо можно посмотреть здесь [2].
Этот проект небольшой, поэтому писать мы будем сразу в REPL [3], онлайн редакторе svelte. Если вам по душе локальная разработка, то можете воспользоваться webpack [4] или rollup [5] шаблонами svelte.
Как альтернативу локальной разработке могу посоветовать онлайн инструмент codesandbox [6].
Если вы используете VScode, то рекомендую установить плагин svelte-vscode [7]
Итак, открываем REPL [3] и начинаем
Сейчас у нас есть файл App.svelte, это точка входа в приложение. Компоненты Svelte стилизуются в теге style [8], как в обычном html. При этом вы получаете изоляцию стилей на уровне компонента. Если необходимо добавить глобальные стили, которые будут доступны "снаружи" объекта, то нужно воспользоваться директивой :global() [9]. Добавим стили и создадим контейнер для нашего приложения.
<style>
:global(body) {
height: 100%;
overscroll-behavior: none; /* отключает pull to refresh*/
user-select: none; /* Отключает выделение в тач интерфейсах */
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
background: rgb(35, 41, 37);
}
:global(html) {
height: 100%;
}
.container {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
</style>
<div class="container">
</div>
Создадим компонент Radar.svelte. Это будет SVG элемент, в котором мы будем рисовать наше колесо.
<svg viewBox="-115 -110 230 220">
</svg>
Javascript код в компоненте Svelte помещается в тег script [10]. Импортируем наш Radar.svelte в App.svelte и отрисуем его.
<script>
import Radar from './Radar.svelte' /* импортируем наш компонент */
</script>
<style>
:global(body) {
height: 100%;
overscroll-behavior: none;
user-select: none;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
background: rgb(35, 41, 37);
}
:global(html) {
height: 100%;
}
.container {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
}
</style>
<div class="container">
<Radar/> <!-- отрисовываем компонент Radar -->
</div>
Сам радар будет состоять из секторов, соответствующих жизненным аспектам. Каждый сектор имеет свой индекс
Каждый сектор состоит из сетки, которая, в свою очередь, является сектором с меньшим размером.
Для отрисовки сектора нам нужно знать координаты трех вершин.
Вершина А всегда с координатами [0, 0], так как начало координат будет по центру нашего радара. Для нахождения вершин В и С воспользуемся функцией из отличного туториала [11] по гексагональным сеткам. На вход функция получает размер сектора и направление, а возвращает строку с координатами 'x,y'.
Создадим файл getHexCorner.js, куда поместим нашу функцию getHexCorner(size, direction)
export default function getHexCorner(size, direction) {
const angleDeg = 60 * direction - 30;
const angleRad = (Math.PI / 180) * angleDeg;
return `${size * Math.cos(angleRad)},${size * Math.sin(angleRad)}`;
}
Теперь создадим компонент сектора Sector.svelte, который рисует сетку. Нам нужен цикл из 10 шагов. В теле компонента svelte не умеет реализовывать цикл for, поэтому я просто сделал массив grid, по которому буду итерировать в директиве #each [12]. Если у вас есть идеи, как это можно сделать элегантней, напишите об этом в комментариях.
<script>
import getHexCorner from "./getHexCorner.js";
export let direction = 0;
const grid = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
</script>
<style>
polygon {
fill: #293038;
stroke: #424a54;
}
</style>
{#each grid as gridValue, i}
<polygon
points={`${getHexCorner(gridValue * 10, direction)}, ${getHexCorner(gridValue * 10, direction + 1)}, 0, 0`}
strokeLinejoin="miter-clip"
stroke-dasharray="4"
stroke-width="0.5" />
{/each}
Импортируем и отрисуем сектор в компоненте Radar.svelte.
<script>
import Sector from './Sector.svelte';
</script>
<svg viewBox="-115 -110 230 220">
<Sector/>
</svg>
Теперь наше приложение отображает 1 сектор.
Чтобы отрисовать весь радар, необходимо знать перечень секторов. Поэтому займемся созданием хранилища состояния. Мы будем использовать кастомный стор [13], в котором реализуем логику обновления состояния. Вообще, это обычное хранилище Svelte [14], которое завернуто в функцию. Это позволяет защитить хранилище от изменений, предоставив набор доступных действий. Мне нравится этот подход тем, что структура данных и логика работы с ними находятся в одном месте.
Создадим файл store.js
Нам потребуются два хранилища:
import { writable } from "svelte/store";
const defaultStore = ["hobby", "friendship", "health", "job", "love", "rich"];
function Radar() {
/* инициализируем хранилище с начальным состоянием */
const { subscribe, update } = writable(defaultStore.map(item=>({name:item, value:0})));
/* возвращаем объект с функцией подписки и доступными действиями */
return {
subscribe,
set: (id, value) =>
update(store =>
store.map(item => (item.name === id ? { ...item, value } : item))
)
};
}
export const radar = Radar();
export const activeSector = writable(null);
Теперь импортируем созданный стор в компонент Radar.svelte и добавим логику отрисовки полного радара.
<script>
import { radar } from "./store.js";
import Sector from "./Sector.svelte";
</script>
<svg viewBox="-115 -110 230 220">
{#each $radar as sector, direction (sector.name)}
<Sector {...sector} {direction} />
{/each}
</svg>
Немного тонкостей директивы #each [15]. Мы используем имя переменной $radar. Директива $ [16] дает понять компилятору Svelte, что наше выражение является хранилищем, и он создает подписку на изменения. Переменная direction хранит индекс текущей итерации, по нему мы будем задавать направление нашего сектора. Выражение (sector.name) указывает svelte на id объекта в итерации [17]. Аналог key в React.
Сейчас наша сетка выглядит вот так
Осталось подготовить сектор к работе с событиями нажатия и перетаскивания.
Событие touchmove, в отличие от mousemove, срабатывает только на элементе, на котором началось. Поэтому мы не сможем отловить момент, когда указатель переместился на другой сектор. Для решения этой проблемы в разметке элемента мы будем хранить текущее имя (name) сектора и его значение (value). В момент события будем определять, какой сектор находится под курсором, и изменять его значение.
Обратите внимание, что Svelte умеет разворачивать конструкцию {varName} в varName={varName}. Это очень упрощает прокидывание свойств.
<script>
import getHexCorner from "./getHexCorner.js";
export let direction = 0;
export let name;
export let value;
const grid = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
</script>
<style>
polygon {
fill: #293038;
stroke: #424a54;
}
.rich {
fill: #469573;
}
.hobby {
fill: #7c3f7a;
}
.friendship {
fill: #5c6bc0;
}
.health {
fill: #e5b744;
}
.job {
fill: #e16838;
}
.love {
fill: #e23f45;
}
</style>
{#each grid as gridValue, i}
<polygon
points={`${getHexCorner(gridValue * 10, direction)}, ${getHexCorner(gridValue * 10, direction + 1)}, 0, 0`}
strokeLinejoin="miter-clip"
stroke-dasharray="4"
stroke-width="0.5"
class={value >= gridValue ? name : ''}
{name}
value={gridValue} />
/>
{/each}
Если мы добавим в нашем сторе (store.js) значение, отличное от нуля, то должен получится такой результат:
Пришло время вдохнуть жизнь в наш радар, создадим обработчик, который на вход принимает ноду, а внутри производит обработку событий касания и мыши.
import { radar, activeSector } from "./store.js";
/* директива get нужна для получения текущего значения хранилища без подписки на само хранилище */
import { get } from "svelte/store";
export default function handleRadar(node) {
const getRadarElementAtPoint = e => {
/* определяем тип события: касание или мышь */
const event = e.touches ? e.touches[0] : e;
const element = document.elementFromPoint(event.pageX, event.pageY);
/* получаем имя и значение сектора из html разметки */
const score = element.getAttribute("value");
const id = element.getAttribute("name");
return { id, score, type: event.type };
};
const start = e => {
/* получаем элемент радара из активного сектора */
const { id } = getRadarElementAtPoint(e);
/* устанавливаем текущий активный сектор */
activeSector.set(id);
};
const end = () => {
/* сбрасываем активный сектор */
activeSector.set(null);
};
const move = e => {
/* тротлинг через requestAnimationFrame поможет избежать лагов при активном перемещении */
window.requestAnimationFrame(() => {
const { id, score, type } = getRadarElementAtPoint(e);
/* проверяем, что у нас есть активный сектор, т.е. движение началось внутри радара, и это не клик */
if (!id || (id !== get(activeSector) && type !== "click") || !score) return;
/* обновляем состояние радара */
radar.set(id, score);
});
};
/* регистрируем обработчики */
node.addEventListener("mousedown", start);
node.addEventListener("touchstart", start);
node.addEventListener("mouseup", end);
node.addEventListener("touchend", end);
node.addEventListener("mousemove", move);
node.addEventListener("touchmove", move);
node.addEventListener("touch", move);
node.addEventListener("click", move);
/* возвращаем объект с функцией destroy, которая произведет отписку от событий при удалении компонента из DOM */
return {
destroy() {
node.removeEventListener("mousedown", start);
node.removeEventListener("touchstart", start);
node.removeEventListener("mouseup", end);
node.removeEventListener("touchend", end);
node.removeEventListener("mousemove", move);
node.removeEventListener("touchmove", move);
node.removeEventListener("touch", move);
node.removeEventListener("click", move);
}
};
}
Теперь просто добавим наш обработчик в svg элемент радара через директиву use: [18]
<script>
import { radar } from "./store.js";
import Sector from "./Sector.svelte";
import handleRadar from "./handleRadar.js";
</script>
<svg viewBox="-115 -110 230 220" use:handleRadar>
{#each $radar as sector, direction (sector.name)}
<Sector {...sector} {direction} />
{/each}
</svg>
Радар теперь реагирует на клики и перетаскивания.
Добавим подписи для секторов и описание
<script>
import getHexCorner from "./getHexCorner.js";
export let name;
export let value;
export let direction;
const grid = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
const flip = direction === 2 || direction === 1;
const radarTranslation = {
hobby: "ХОББИ",
friendship: "ДРУЖБА",
health: "ЗДОРОВЬЕ",
job: "РАБОТА",
love: "ЛЮБОВЬ",
rich: "БЛАГОСОСТОЯНИЕ"
};
</script>
<style>
polygon {
fill: #293038;
stroke: #424a54;
}
text {
font-size: 8px;
fill: white;
}
.value {
font-weight: bold;
font-size: 12px;
}
.rich {
fill: #469573;
}
.hobby {
fill: #7c3f7a;
}
.friendship {
fill: #5c6bc0;
}
.health {
fill: #e5b744;
}
.job {
fill: #e16838;
}
.love {
fill: #e23f45;
}
</style>
{#each grid as gridValue, i}
<polygon
points={`${getHexCorner(gridValue * 10, direction)}, ${getHexCorner(gridValue * 10, direction + 1)}, 0, 0`}
strokeLinejoin="miter-clip"
stroke-dasharray="4"
stroke-width="0.5"
class={value >= gridValue ? name : ''}
{name}
value={gridValue} />
{/each}
<g
transform={`translate(${getHexCorner(105, flip ? direction + 1 : direction)}) rotate(${direction * 60 + (flip ? -90 : 90)})`}>
<text x="50" y={flip ? 5 : 0} text-anchor="middle">
{radarTranslation[name]}
</text>
<text x="50" y={flip ? 18 : -10} text-anchor="middle" class="value">
{value}
</text>
</g>
Радар должен выглядеть так.
Я немного расширил функционал радара, добавил хранение данных в localStorage и составление плана действий. Вы можете попробовать приложение life-checkup [19], исходный код доступен в gitlab [20].
Автор: Alexander Zinchenko
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/334693
Ссылки в тексте:
[1] Полная Ж: https://www.litres.ru/radislav-gandapas/polnaya-zh-zhizn-kak-biznes-proekt
[2] здесь: https://svelte.dev/repl/3afce9affec54b49919fa3c76949904d?version=3.12.1
[3] REPL: https://svelte.dev/repl/
[4] webpack: https://github.com/sveltejs/template-webpack
[5] rollup: https://github.com/sveltejs/template
[6] codesandbox: https://codesandbox.io/s/svelte
[7] svelte-vscode: https://marketplace.visualstudio.com/items?itemName=JamesBirtles.svelte-vscode
[8] style: https://svelte.dev/tutorial/styling
[9] :global(): https://svelte.dev/docs#style
[10] script: https://svelte.dev/docs#script
[11] отличного туториала: https://www.redblobgames.com/grids/hexagons/#basics
[12] #each: https://svelte.dev/tutorial/each-blocks
[13] кастомный стор: https://svelte.dev/tutorial/custom-stores
[14] хранилище Svelte: https://svelte.dev/tutorial/writable-stores
[15] #each: https://svelte.dev/docs#each
[16] Директива $: https://svelte.dev/tutorial/store-bindings
[17] id объекта в итерации: https://svelte.dev/tutorial/keyed-each-blocks
[18] use:: https://svelte.dev/docs#use_action
[19] life-checkup: https://life-checkup.web.app
[20] gitlab: https://gitlab.com/az67128/svelte-life
[21] Источник: https://habr.com/ru/post/458974/?utm_source=habrahabr&utm_medium=rss&utm_campaign=458974
Нажмите здесь для печати.