Переглянути джерело

Continue working on project.

Cixo Develop 8 місяців тому
батько
коміт
18699f9324
43 змінених файлів з 1823 додано та 4 видалено
  1. 9 3
      index.html
  2. 5 0
      scripts/assets/abstract.js
  3. 65 0
      scripts/assets/types.js
  4. 43 0
      scripts/assets/view.js
  5. 18 0
      scripts/interface/container.js
  6. 13 0
      scripts/interface/icon.js
  7. 12 0
      scripts/interface/image-icon.js
  8. 36 0
      scripts/interface/image.js
  9. 16 0
      scripts/interface/material-icon.js
  10. 55 0
      scripts/interface/push.js
  11. 19 0
      scripts/interface/text.js
  12. 104 0
      scripts/interface/widget.js
  13. 12 0
      scripts/loader.js
  14. 40 0
      scripts/selector/full-screen-selector.js
  15. 23 0
      scripts/selector/mode-selector.js
  16. 31 0
      scripts/selector/selector-item.js
  17. 26 0
      scripts/selector/selector.js
  18. 72 0
      scripts/settings/color-mode.js
  19. 85 0
      scripts/settings/option.js
  20. 73 0
      scripts/space/actor.js
  21. 22 0
      scripts/space/container.js
  22. 139 0
      scripts/space/coordinates.js
  23. 28 0
      scripts/space/core.js
  24. 23 0
      scripts/space/factor.js
  25. 33 0
      scripts/space/functional_factor.js
  26. 14 0
      scripts/space/icons.js
  27. 77 0
      scripts/space/moving.js
  28. 63 0
      scripts/space/position.js
  29. 38 0
      scripts/space/push.js
  30. 140 0
      scripts/space/render-engine.js
  31. 50 0
      scripts/space/room.js
  32. 168 0
      scripts/space/scene-ui.js
  33. 27 0
      scripts/space/scene.js
  34. 84 0
      theme/icons/shelf.svg
  35. 98 0
      theme/icons/space.svg
  36. 0 0
      theme/style/app.css
  37. 1 0
      theme/style/colors.css
  38. 0 0
      theme/style/font.css
  39. 5 0
      theme/style/icon.css
  40. 3 1
      theme/style/loader.css
  41. 53 0
      theme/style/mode-selector.css
  42. 0 0
      theme/style/push.css
  43. 0 0
      theme/style/scene.css

+ 9 - 3
index.html

@@ -3,17 +3,23 @@
 <html>
     <head>
         <meta charset="UTF-8">
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <script type="importmap">
             {
               "imports": {
-                "space": "./assets/space/",
+                "assets/": "./scripts/assets/",
+                "space/": "./scripts/space/",
+                "shelf/": "./scripts/shelf/",
+                "selector/": "./scripts/selector/",
+                "settings/": "./scripts/settings/",
+                "interface/": "./scripts/interface/",
                 "three-js": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
                 "three-js/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/"
               }
             }
         </script>
-        <script type="module" src="assets/core.js"></script>
-        <link rel="stylesheet" type="text/css" href="theme/loader.css">
+        <script type="module" src="scripts/loader.js"></script>
+        <link rel="stylesheet" type="text/css" href="theme/style/loader.css">
         <link rel="preconnect" href="https://fonts.googleapis.com">
         <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
         <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet">

+ 5 - 0
scripts/assets/abstract.js

@@ -0,0 +1,5 @@
+const abstract = () => {
+    return new Error("This is abstract function. It must be overwriten.");
+};
+
+export { abstract };

+ 65 - 0
scripts/assets/types.js

@@ -0,0 +1,65 @@
+class types {
+    static check_string(target, none = false) {
+        if (none && (target === undefined || target === null)) {
+            return;
+        }
+
+        if (typeof(target) !== "string") {
+            throw new TypeError("Target must be string.");
+        }
+    }
+
+    static check_number(target, none = false) {
+        if (none && (target === undefined || target === null)) {
+            return;
+        }
+
+        if (typeof(target) !== "number") {
+            throw new TypeError("Target must be number.");
+        }
+    }
+
+    static check_html_element(target, none = false) {
+        if (none && (target === undefined || target === null)) {
+            return;
+        }
+
+        if (target instanceof HTMLElement) {
+            return;
+        }
+
+        throw new TypeError("Target must be an HTML Element.");
+    }
+
+    static check_callback(target, none = false) {
+        if (none && (target === undefined || target === null)) {
+            return;
+        }
+
+        if (typeof(target) !== "function") {
+            throw new TypeError("Target must be an callback.");
+        }
+    }
+
+    static check_boolean(target, none = false) {
+         if (none && (target === undefined || target === null)) {
+            return;
+        }
+
+        if (typeof(target) !== "boolean") {
+            throw new TypeError("Target must be an boolean.");
+        }
+    }
+
+    static block_strings(target, blocked) {
+        for (const string in blocked) {
+            if (target.search(string) !== -1) {
+                const fail = "Target can not contain \"" + string + "\".";
+
+                throw new TypeError(fail);
+            }
+        }
+    }
+}
+
+export { types };

+ 43 - 0
scripts/assets/view.js

@@ -0,0 +1,43 @@
+import { abstract } from "assets/abstract.js";
+import { types } from "assets/types.js";
+
+class view {
+    #app;
+    #name;
+
+    constructor(app, name = undefined) {
+        types.check_html_element(app);
+        types.check_string(name, true);
+
+        this.#app = app;
+        this.#name = name;
+    }
+
+    get has_name() {
+        return this.#name !== undefined && this.#name !== null;
+    }
+
+    get name() {
+        return this.#name;
+    }
+
+    get app() {
+        return this.#app;
+    }
+
+    _clean() {
+        while (this.app.hasChildNodes()) {
+            this.app.firstChild.remove();
+        }
+    }
+
+    hide() {
+        this._clean();
+    }
+
+    show() {
+        throw abstract();
+    }
+}
+
+export { view };

+ 18 - 0
scripts/interface/container.js

@@ -0,0 +1,18 @@
+import { types } from "assets/types.js";
+import { widget } from "interface/widget.js";
+
+class container extends widget {
+    constructor(name = undefined) {
+        super(document.createElement("div"), "container", name);
+    }
+
+    append(target) {
+        this._append(target);
+    }
+
+    clean() {
+        this._drop_content();
+    }
+}
+
+export { container }

+ 13 - 0
scripts/interface/icon.js

@@ -0,0 +1,13 @@
+import { widget } from "interface/widget.js";
+
+class icon extends widget {
+    constructor(target, name = undefined) {
+        super(target, "icon", name);
+    }
+    
+    change(target) {
+        this._content = target;
+    }
+}
+
+export { icon };

+ 12 - 0
scripts/interface/image-icon.js

@@ -0,0 +1,12 @@
+import { types } from "assets/types.js";
+import { icon } from "interface/icon.js";
+import { image } from "interface/image.js";
+
+class image_icon extends icon {
+    constructor(source, name = undefined) {
+        super(document.createElement("img"), name);
+        this._content = source;
+    }    
+}
+
+export { image_icon };

+ 36 - 0
scripts/interface/image.js

@@ -0,0 +1,36 @@
+import { types } from "assets/types.js";
+import { widget } from "interface/widget.js";
+
+class image extends widget {
+    constructor(source, description, name = undefined) {
+        types.check_string(source);
+        types.check_string(description, true);
+
+        if (description === undefined && description === null) {
+            description = "";
+        }
+
+        const target = document.createElement("img");
+
+        target.src = source;
+        target.alt = description;
+
+        super(target, "image", name);
+    }   
+
+    set source(target) {
+        this._content = target;
+    }
+
+    set description(target) {
+        types.check_string(target, true);
+
+        if (target === undefined) {
+            target = "";
+        }
+
+        this.node.alt = target;
+    }
+}
+
+export { image };

+ 16 - 0
scripts/interface/material-icon.js

@@ -0,0 +1,16 @@
+import { types } from "assets/types.js";
+import { icon } from "interface/icon.js";
+
+class material_icon extends icon {
+    constructor(which, name = undefined) {
+        types.check_string(which);
+
+        const target = document.createElement("span");
+        target.classList.add("meterial-symbols-outlined");
+        target.innerText = which;
+
+        super(target, "material-icon", name);
+    }
+} 
+
+export { material_icon };

+ 55 - 0
scripts/interface/push.js

@@ -0,0 +1,55 @@
+import { types } from "assets/types.js";
+import { widget } from "interface/widget.js";
+
+class push extends widget {
+    #is_pressed;
+
+    constructor(name = undefined, submit = false) {
+        types.check_boolean(submit);
+
+        const target = document.createElement("button");
+
+        if (submit) {
+            target.type = "submit";
+        }
+
+        if (typeof(name) === "string") {
+            target.name = name;
+        }
+        
+        super(target, "push", name);
+    
+        this.#is_pressed = false;
+        this._add_event("mousedown", () => { this.#pressed(); });
+        this._add_event("mouseup", () => { this.#released(); });
+        this._add_event("mouseover", () => { this.#released(); });
+    }
+
+    #pressed() {
+        this.#is_pressed = true;
+    }
+
+    #released() {
+        this.#is_pressed = false;
+    }
+
+    set icon(target) {
+        if (!(target instanceof icon)) {
+            throw new TypeError("Icon for button must be an icon.");
+        }
+
+        this._drop_content();
+        this._append(target);
+    }
+
+    set text(target) {
+        this._drop_content();
+        this._content = text;
+    }
+
+    get is_pressed() {
+        return this.#is_pressed;
+    }
+}
+
+export { push };

+ 19 - 0
scripts/interface/text.js

@@ -0,0 +1,19 @@
+import { types } from "assets/types.js";
+import { widget } from "interface/widget.js";
+
+class text extends widget {
+    constructor(content, name = undefined) {
+        types.check_string(content);
+
+        const target = document.createElement("p");
+        target.innerText = content;
+
+        super(target, "text", name);
+    }
+
+    set content(target) {
+        this._content = target;
+    }
+}
+
+export { text };

+ 104 - 0
scripts/interface/widget.js

@@ -0,0 +1,104 @@
+import { types } from "assets/types.js";
+
+class widget {
+    #node;
+
+    constructor(node, classname, name) {
+        types.check_html_element(node);
+        types.check_string(name, true);
+        types.check_string(classname);
+
+        node.classList.add(classname);
+
+        if (name !== undefined && name !== null) {
+            types.block_strings(name, [" ", "#", "\t", "\n"]);
+            
+            node.classList.add(name);
+            node.classList.add(classname + "-" + name);
+        }
+
+        this.#node = node;
+    }
+
+    get node() {
+        if (!(this.#node instanceof HTMLElement)) {
+            throw new ReferenceError("Widget had been removed.");
+        }
+
+        return this.#node;
+    }
+
+    _add_event(name, callback) {
+        types.check_string(name);
+        types.check_callback(callback);
+
+        this.node.addEventListener(name, (event) => {
+            callback(event, this);
+        });
+    }
+
+    set _content(target) {
+        types.check_string(target);
+
+        const node = this.node;
+
+        if (node instanceof HTMLInputElement) {
+            node.value = target
+            return;
+        }
+
+        if (node instanceof HTMLImageElement) {
+            node.src = target;
+            return;
+        }
+
+        node.innetText = target;
+    }
+
+    _drop_content() {
+        const node = this.node;
+
+        if (node instanceof HTMLInputElement) {
+            node.value = "";
+            return;
+        }
+
+        while (node.childElementCound) {
+            node.lastChild.remove();
+        }
+    }
+
+    place(target) {
+        types.check_html_element(target);
+
+        target.appendChild(this.node);
+    }
+
+    _append(target) {
+        if (this.node instanceof HTMLInputElement) {
+            throw new ReferenceError("Can not append into input element.");
+        }
+
+        if (target instanceof HTMLElement) {
+            this.node.appendChild(target);
+            return;
+        }
+
+        if (target instanceof widget) {
+            this.node.appendChild(target.node);
+            return;
+        }
+
+        throw new TypeError("New item must be HTML element or widget.");
+    }
+
+    add_click(callback) {
+        this._add_event("click", callback);
+    }
+
+    get style() {
+        return this.node.style;
+    }
+}
+
+export { widget };

+ 12 - 0
scripts/loader.js

@@ -0,0 +1,12 @@
+import { mode_selector } from "selector/mode-selector.js";
+import { color_mode } from "settings/color-mode.js";
+
+document.addEventListener("DOMContentLoaded", () => {
+    const app = document.querySelector(".app");
+
+    const color_setting = new color_mode(app);
+    color_setting.update();
+
+    const selector = new mode_selector(app);
+    selector.show();
+});

+ 40 - 0
scripts/selector/full-screen-selector.js

@@ -0,0 +1,40 @@
+import { selector } from "./selector.js";
+import { selector_item } from "./selector-item.js"
+import { container } from "interface/container.js";
+import { image } from "interface/image.js";
+import { text } from "interface/text.js";
+import { image_icon } from "interface/image-icon.js";
+
+class full_screen_selector extends selector {
+    #option(item) {
+        const space = new container("selector-item");
+        const miniature = new image_icon(item.icon);
+        const title = new text(item.title);
+
+        space.append(miniature);
+        space.append(title);
+        space.add_click(() => { item.action(); });
+
+        return space;
+    }
+
+    show() {
+        this._clean();
+       
+        let name = "full-screen-selector";
+
+        if (this.has_name) {
+            name = this.name;
+        }
+
+        const target = new container(name);
+
+        this.items.forEach((item) => {
+            target.append(this.#option(item));
+        });
+
+        this.app.appendChild(target.node);
+    }
+}
+
+export { full_screen_selector };

+ 23 - 0
scripts/selector/mode-selector.js

@@ -0,0 +1,23 @@
+import { full_screen_selector } from "./full-screen-selector.js";
+import { selector } from "./selector.js";
+import { selector_item } from "./selector-item.js";
+
+class mode_selector extends full_screen_selector {
+    constructor(app) {
+        super(app, "mode-selector");
+
+        this.add(new selector_item(
+            "Shelf",
+            "./theme/icons/shelf.svg",
+            () => {}
+        ));
+
+        this.add(new selector_item(
+            "Space",
+            "./theme/icons/space.svg",
+            () => {}
+        ));
+    }
+}
+
+export { mode_selector };

+ 31 - 0
scripts/selector/selector-item.js

@@ -0,0 +1,31 @@
+import { types } from "assets/types.js";
+
+class selector_item {
+    #title;
+    #icon;
+    #action;
+
+    constructor(title, icon, action) {
+        types.check_string(title);
+        types.check_string(icon);
+        types.check_callback(action);
+
+        this.#title = title;
+        this.#icon = icon;
+        this.#action = action;
+    }
+
+    get title() {
+        return this.#title;
+    }
+
+    get icon() {
+        return this.#icon;
+    }
+
+    get action() {
+        return this.#action;
+    }
+}
+
+export { selector_item };

+ 26 - 0
scripts/selector/selector.js

@@ -0,0 +1,26 @@
+import { view } from "../assets/view.js";
+import { selector_item } from "./selector-item.js";
+
+class selector extends view {
+    #items;
+
+    constructor(app, name = undefined) {
+        super(app, name);
+
+        this.#items = new Array();
+    }
+   
+    add(item) {
+        if (!(item instanceof selector_item)) {
+            throw new TypeError("Could add only selector item.");
+        }
+
+        this.#items = [...this.#items, item];
+    }
+
+    get items() {
+        return this.#items;
+    }
+}
+
+export { selector };

+ 72 - 0
scripts/settings/color-mode.js

@@ -0,0 +1,72 @@
+import { types } from "assets/types.js";
+import { option } from "settings/option.js";
+
+const color_modes = Object.freeze({
+    DARK: "dark",
+    LIGHT: "light",
+})
+
+class color_mode extends option {
+    #app;
+    #dark_class;
+    #light_class;
+
+    constructor(
+        app, 
+        dark_class = "darkmode", 
+        light_class = "lightmode"
+    ) {
+        super("color-mode");
+
+        types.check_html_element(app);
+        types.check_string(dark_class);
+        types.check_string(light_class);
+
+        if (dark_class === light_class) {
+            throw new Error("Dark modeclass and lightmode class is same.");
+        }
+
+        this._add_option(color_modes.DARK);
+        this._add_option(color_modes.LIGHT);
+        this._default = color_modes.DARK;
+        
+        this.#app = app;
+        this.#dark_class = dark_class;
+        this.#light_class = light_class;
+        this.update();
+    }
+
+    reverse() {
+        if (this.selected === color_modes.DARK) {
+            this.select(color_modes.LIGHT);
+        } else {
+            this.select(color_modes.DARK);
+        }
+
+        this.update();
+    }
+
+    update() {
+        const app = this.#app;
+        const darkmode = this.#dark_class;
+        const lightmode = this.#light_class;
+        const current = this.selected;
+
+        if (app.classList.contains(darkmode)) {
+            app.classList.remove(darkmode);
+        }
+
+        if (app.classList.contains(lightmode)) {
+            app.classList.remove(lightmode);
+        }
+
+        if (current === color_modes.DARK) {
+            app.classList.add(darkmode);
+            return;
+        }
+
+        app.classList.add(lightmode);
+    }
+}
+
+export { color_mode, color_modes };

+ 85 - 0
scripts/settings/option.js

@@ -0,0 +1,85 @@
+import { types } from "assets/types.js";
+
+class option {
+    #name;
+    #options;
+    #default;
+
+    constructor(name) {
+        types.check_string(name);
+
+        name = name.trim();
+
+        if (name.length === 0) {
+            throw new TypeError("Name can not be blank.");
+        }
+
+        this.#name = name;
+        this.#default = undefined;
+        this.#options = new Set();
+    }
+
+    #sanitize_option(option) {
+        types.check_string(option);
+
+        option = option.trim();
+
+        if (option.length === 0) {
+            throw new TypeError("Option could not be blank.");
+        }
+
+        return option;
+    }
+
+    set _default(option) {
+        option = this.#sanitize_option(option);
+        
+        this.#default = option;
+        this.#options.add(option);
+    }
+
+    _add_option(option) {
+        this.#options.add(this.#sanitize_option(option));
+    }
+
+    get options() {
+        return Array.from(this.#options.values());
+    }
+
+    get default() {
+        if (this.#default === undefined) {
+            throw new ReferenceError("Default option is not setup yet.");
+        }
+
+        return this.#default;
+    }
+
+    select(option) {
+        if (!this.#options.has(option)) {
+            throw new Error("Setting has not option \"" + option + "\".");
+        }
+
+        if (this.default === option) {
+            localStorage.removeItem(this.#name);
+            return;
+        }
+
+        localStorage.setItem(this.#name, option);
+    }
+    
+    get selected() {
+        const read = localStorage.getItem(this.#name);
+
+        if (read === null) {
+            return this.default;
+        }
+
+        if (!this.#options.has(read)) {
+            return this.default;
+        }
+
+        return read;
+    }
+}
+
+export { option };

+ 73 - 0
scripts/space/actor.js

@@ -0,0 +1,73 @@
+import { coordinates } from "./coordinates.js";
+import { position } from "./position.js";
+import { moving } from "./moving.js";
+
+class actor extends coordinates {
+    #movement;
+    #rotation;
+    #speed;
+
+    constructor() {
+        super();
+
+        this.#speed = 0.25;    
+        this.#movement = new moving();
+        this.#rotation = new moving();
+    }
+
+    set speed(target) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Speed must be an number.");
+        }
+
+        this.#speed = target;
+    }
+
+    get speed() {
+        return this.#speed;
+    }
+
+    rotate_cursor(cursor, size, scale = 2) {
+        if (!(cursor instanceof position)) {
+            throw new TypeError("Cursor compare must be an position object.");
+        }
+
+        if (!(size instanceof position)) {
+            throw new TypeError("Size must be an position object.");
+        }
+
+        if (typeof(scale) !== "number") {
+            throw new TypeError("Scale must be an number.");
+        }
+
+        const rotate = cursor.x / size.x * 360 * scale;
+        const bottom_rotate = cursor.y / size.y * 180 * scale;
+        
+        this.rotate_bottom(bottom_rotate);
+        this.rotate_clockwise(rotate);
+    }
+
+    get movement() {
+        return this.#movement;
+    }
+
+    get rotation() {
+        return this.#rotation;
+    }
+
+    update() {
+        this.#update_position();
+        this.#update_rotation();
+    }
+
+    #update_rotation() {
+        this.rotate_clockwise(this.#speed * 10 * this.rotation.z);
+    }
+
+    #update_position() {
+        this.move_front(this.#speed * this.movement.y);
+        this.move_right(this.#speed * this.movement.x);
+    }
+}
+
+export { actor };

+ 22 - 0
scripts/space/container.js

@@ -0,0 +1,22 @@
+const container = (name, customisation = null) => {
+    if (typeof(name) !== "string") {
+        throw new TypeError("Name of the container must be string.");
+    }
+
+    if (customisation !== null && typeof(customisation) !== "function") {
+        throw new TypeError("Customisation must be null or function.");
+    }
+
+    const target = document.createElement("div");
+
+    target.classList.add("container");
+    target.id = "container-" + name;
+
+    if (customisation) {
+        customisation(target);
+    }
+
+    return target;
+};
+
+export { container };

+ 139 - 0
scripts/space/coordinates.js

@@ -0,0 +1,139 @@
+import { position } from "./position.js";
+
+class coordinates extends position {
+    #rotate;
+    #head_rotate;
+
+    constructor() {
+        super();
+        
+        this.#rotate = 0;
+        this.#head_rotate = 0;
+    }
+
+    get rotate() {
+        this.#check_rotate();
+        
+        return this.#rotate;
+    }
+
+    set rotate(target) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Coordinate must be number.");
+        }
+
+        this.#rotate = target;
+        this.#check_rotate();
+    }
+
+    #check_rotate() {
+        while (this.#rotate < 0) {
+            this.#rotate += 360;
+        }
+
+        this.#rotate = this.#rotate % 360;
+    }
+
+    set head_rotate(target) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Coordinate must be number.");
+        }
+
+        this.#head_rotate = target % 90;
+        this.#check_head_rotate();
+    }   
+
+    #check_head_rotate() {
+        if (this.#head_rotate > 90) this.#head_rotate = 90;
+        if (this.#head_rotate < -90) this.#head_rotate = -90;
+    }
+
+    get head_rotate() {
+        this.#check_head_rotate();
+
+        return this.#head_rotate;
+    }
+
+    #radians(change = 0) {
+        return (this.rotate + change) * Math.PI / 180;
+    }
+
+    move_front(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Steps must be number.");
+        }
+    
+        this.y -= Math.cos(this.#radians()) * target;
+        this.x -= Math.sin(this.#radians()) * target;
+    }
+
+    move_back(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Steps must be number.");
+        }
+
+        this.move_front(-target);
+    }
+
+    move_left(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Steps must be number.");
+        }
+        
+        this.y += Math.cos(this.#radians(-90)) * target;
+        this.x += Math.sin(this.#radians(-90)) * target;
+    }
+
+    move_right(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Steps must be number.");
+        }
+
+        this.move_left(-target);
+    }
+
+    rotate_clockwise(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Degrees must be number.");
+        }
+
+        this.#rotate -= target;
+    }
+
+    rotate_counterclockwise(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Degrees must be number.");
+        }
+
+        this.#rotate += target;
+    }
+
+    rotate_top(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Degrees must be number.");
+        }
+
+        this.#head_rotate += target;
+    }
+
+    rotate_bottom(target = 1) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Degrees must be number.");
+        }
+
+        this.#head_rotate -= target;
+    }
+
+    get as_string() {
+        let dump = "";
+        dump += "X: " + this.x + "\n";
+        dump += "Y: " + this.y + "\n";
+        dump += "Z: " + this.z + "\n";
+        dump += "ROTATE: " + this.rotate;
+        dump += "HEAD ROTATE: " + this.head_rotate;
+
+        return dump;
+    }
+}
+
+export { coordinates };

+ 28 - 0
scripts/space/core.js

@@ -0,0 +1,28 @@
+import { color_mode } from "./color-mode.js";
+import { push } from "./push.js";
+import { material_icon } from "./icons.js";
+import { scene } from "./scene.js";
+import { scene_ui } from "./scene-ui.js";
+import { room } from "./room.js";
+
+document.addEventListener("DOMContentLoaded", () => {
+    const app = document.querySelector(".app");
+    const colors_state = new color_mode(app);
+    const colors_changer = push("change-color", () => {
+        colors_state.reverse();
+    }, (target) => {
+        target.innerText = "";
+        target.appendChild(material_icon("invert_colors"));
+    });
+
+    const space = new scene(room);
+    const controls = new scene_ui(space);
+
+    space.background = 0x303030;
+
+    app.appendChild(space.canvas);
+    app.appendChild(controls.box);
+    app.appendChild(colors_changer);
+
+    space.run();
+});

+ 23 - 0
scripts/space/factor.js

@@ -0,0 +1,23 @@
+import * as three from "three-js";
+
+class factor {
+    #mesh;
+
+    constructor(mesh) {
+        if (!(mesh instanceof three.Object3D)) {
+            throw new TypeError("Must initialize with Object3D.");
+        }
+
+        this.#mesh = mesh;
+    }
+
+    get mesh() {
+        return this.#mesh;
+    }
+
+    loop() {
+        return null;
+    }
+}
+
+export { factor }

+ 33 - 0
scripts/space/functional_factor.js

@@ -0,0 +1,33 @@
+import * as three from "three-js";
+import { factor } from "./factor.js";
+
+class functional_factor extends factor {
+    #animation;
+
+    constructor(init, animation = null) {
+        if (typeof (init) !== "function") {
+            throw new TypeError("Init must be an function.");
+        }
+
+        if (animation !== null && typeof (animation) !== "function") {
+            throw new TypeError("Animation could only be null or function.");
+        }
+
+        const mesh = init();
+
+        if (!(mesh instanceof three.Object3D)) {
+            throw new TypeError("Factor initializer must return Object3D.");
+        }
+
+        super(mesh);
+        this.#animation = animation;
+    }
+
+    loop() {
+        if (this.#animation !== null) {
+            this.#animation(this.mesh);
+        }
+    }
+}
+
+export { functional_factor };

+ 14 - 0
scripts/space/icons.js

@@ -0,0 +1,14 @@
+const material_icon = (name) => {
+    if (typeof(name) !== "string") {
+        throw new TypeError("Name of the icon must be string.");
+    }
+
+    const target = document.createElement("span");
+    
+    target.classList.add("material-symbols-outlined");
+    target.innerText = name;
+
+    return target;
+};
+
+export { material_icon };

+ 77 - 0
scripts/space/moving.js

@@ -0,0 +1,77 @@
+class move_direction {
+    static get add() {
+        return 1;
+    }
+
+    static get stop() {
+        return 0;
+    }
+
+    static get sub() {
+        return -1;
+    }
+
+    static contains(target) {
+        if (target === move_direction.add) return true;
+        if (target === move_direction.stop) return true;
+        if (target === move_direction.sub) return true;
+    
+        return false;
+    }
+}
+
+class moving {
+    #x;
+    #y;
+    #z;
+
+    constructor() {
+        this.#x = 0;
+        this.#y = 0;
+        this.#z = 0;
+    }
+
+    get x() {
+        return this.#x;
+    }
+
+    get y() {
+        return this.#y;
+    }
+
+    get z() {
+        return this.#z;
+    }
+
+    set x(target) {
+        if (!move_direction.contains(target)) {
+            throw new TypeError("New direction must be from move direction.");
+        }
+
+        this.#x = target;
+    }
+
+    set y(target) {
+        if (!move_direction.contains(target)) {
+            throw new TypeError("New direction must be from move direction.");
+        }
+
+        this.#y = target;
+    }
+
+    set z(target) {
+        if (!move_direction.contains(target)) {
+            throw new TypeError("New direction must be from move direction.");
+        }
+
+        this.#z = target;
+    }
+
+    stop() {
+        this.x = move_direction.stop;
+        this.y = move_direction.stop;
+        this.z = move_direction.stop;
+    }
+}
+
+export { moving, move_direction }

+ 63 - 0
scripts/space/position.js

@@ -0,0 +1,63 @@
+class position {
+    #x;
+    #y;
+    #z;
+
+    constructor(x = 0, y = 0, z = 0) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    set x(target) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("X cord must be an number.");
+        }
+
+        this.#x = target;
+    }
+
+    set y(target) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Y cord must be an number.");
+        }
+
+        this.#y = target;
+    }
+    
+    set z(target) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Z cord must be an number.");
+        }
+
+        this.#z = target;
+    }
+
+    get x() {
+        return this.#x;
+    }
+
+    get y() {
+        return this.#y;
+    }
+
+    get z() {
+        return this.#z;
+    }
+
+    compare(target) {
+        if (!(target instanceof position)) {
+            throw new TypeError("Can only compare position with position.");   
+        }
+
+        const result = new position();
+
+        result.x = target.x - this.x;
+        result.y = target.y - this.y;
+        result.z = target.z - this.z;
+
+        return result;
+    }
+}
+
+export { position };

+ 38 - 0
scripts/space/push.js

@@ -0,0 +1,38 @@
+const push = (name, action, customisation = null) => {
+    if (action !== null && typeof(action) !== "function") {
+        throw new TypeError("Action must be an function.");
+    }
+
+    if (typeof(name) !== "string") {
+        throw new TypeError("Name of the push must be string.");
+    }
+
+    if (customisation !== null && typeof(customisation) !== "function") {
+        throw new TypeError("Customisation must be null or function");
+    }
+
+    const target = document.createElement("button");
+    
+    target.type = "button";
+    target.name = name;
+    target.id = "push-" + name;
+    
+    target.innerText = name
+        .toUpperCase()
+        .replaceAll("-", " ")
+        .replaceAll("_", " ");
+
+    target.classList.add("push");
+    
+    if (action !== null) {
+        target.addEventListener("click", action);
+    }
+
+    if (customisation) {
+        customisation(target);
+    }
+
+    return target;
+};
+
+export { push };

+ 140 - 0
scripts/space/render-engine.js

@@ -0,0 +1,140 @@
+import * as three from "three-js";
+import { actor } from "./actor.js";
+import { factor } from "./factor.js";
+
+class render_engine {
+    #renderer;
+    #scene;
+    #camera;
+    #canvas;
+    #actors;
+    #player;
+    #looped;
+
+    constructor(canvas, context, player) {
+        if (!(canvas instanceof HTMLCanvasElement)) {
+            throw new TypeError("Canvas must be an HTMLCanvasElement.");
+        }
+
+        if (!(context instanceof WebGL2RenderingContext)) {
+            throw new TypeError("Context must be WebGL RenderingContext.");
+        }
+
+        if (!(player instanceof actor)) {
+            throw new TypeError("Player must be an actor.");
+        }
+
+        this.#actors = new Array();
+        this.#player = player;
+        this.#canvas = canvas;
+        this.#scene = new three.Scene();
+        this.#camera = new three.PerspectiveCamera(75, 1, 0.1, 1000);
+
+        this.#renderer = new three.WebGLRenderer({ 
+            canvas: this.#canvas,
+            context: context
+        });
+
+        this.#renderer.shadowMap.enable = true;
+        this.#renderer.shadowMap.type = three.BasicShadowMap;
+   
+        this.#resize_canvas();
+        this.#update_camera();
+
+        window.addEventListener("resize", () => {
+            this.#resize_canvas();
+            this.#update_camera();
+        });
+    }
+
+    get player() {
+        return this.#player;
+    }
+
+    get canvas() {
+        return this.#canvas;
+    }
+
+    set background(target) {
+        if (typeof(target) !== "number") {
+            throw new TypeError("Background must be an number.");
+        }
+
+        this.#scene.background = target;
+    }
+
+    get background() {
+        return this.#scene.background;
+    }
+
+    #resize_canvas() {
+        this.#canvas.width = window.innerWidth;
+        this.#canvas.height = window.innerHeight;
+
+        this.#renderer.setSize(this.#canvas.width, this.#canvas.height);
+    }
+
+    #update_camera() {
+        this.#camera.aspect = this.#canvas.width / this.#canvas.height;
+        this.#camera.updateProjectionMatrix();
+    }
+
+    run() {
+        this.#looped = true;
+        this.#loop();
+    }
+
+    stop() {
+        this.#looped = false;
+    }
+
+    #loop() {
+        if (!this.#looped) {
+            return;
+        }
+
+        const start = performance.now();
+        this.#render();
+        const stop = performance.now();
+        const tooked = stop - start;
+        const new_frame = 1000 / 60 - tooked;
+
+        if (new_frame <= 0) {
+            setTimeout(() => { this.#loop(); }, 1);
+            return;
+        } 
+
+        setTimeout(() => { this.#loop(); }, new_frame);
+    }
+
+    add_factor(target) {
+        if (!(target instanceof factor)) {
+            throw new TypeError("New factor must be in factor class.");
+        }
+
+        this.#actors.push(target);
+        this.#scene.add(target.mesh);
+    }
+
+    #sync_camera() {
+        this.#player.update();
+
+        this.#camera.position.x = this.#player.x;
+        this.#camera.position.z = this.#player.y;
+        
+        this.#camera.rotation.y 
+        = three.MathUtils.degToRad(this.#player.rotate);
+    }
+
+    #render() {
+        this.#sync_camera();
+
+        this.#actors.forEach(actor => {
+            actor.loop();
+        });
+
+        this.#renderer.render(this.#scene, this.#camera);
+    }
+}
+
+export { render_engine };

+ 50 - 0
scripts/space/room.js

@@ -0,0 +1,50 @@
+import * as three from "three-js";
+import { functional_factor } from "./functional_factor.js";
+
+const room = (space) => {
+    const cube = new functional_factor(() => {
+        const material = new three.MeshStandardMaterial({
+            color: 0xA000A0
+        });
+
+        const geometry = new three.BoxGeometry(1, 1, 1);
+        const mesh = new three.Mesh(geometry, material);
+
+        mesh.position.x = 10;
+        mesh.position.z = -10;
+        mesh.position.y = 1;
+
+        return mesh;
+    }, (item) => {
+        item.rotation.x += 0.01;    
+    });
+
+    const light = new functional_factor(() => {
+        const light = new three.HemisphereLight(0x707070);
+        
+        light.position.x = -10;
+        light.position.z = 10;
+        light.position.y = 10;
+
+        return light;
+    }, (item) => {
+        if (item.rotation.x == 0) {
+            item.position.y += 0.1;
+        } else {
+            item.position.y -= 0.1;
+        }
+
+        if (item.position.y > 10) {
+            item.rotation.x = 0.1;
+        }
+
+        if (item.position.y < 0) {
+            item.rotation.x = 0;
+        }
+    });
+
+    space.add_factor(cube);
+    space.add_factor(light);
+};
+
+export { room };

+ 168 - 0
scripts/space/scene-ui.js

@@ -0,0 +1,168 @@
+import { scene } from "./scene.js";
+import { push } from "./push.js";
+import { material_icon } from "./icons.js";
+import { container } from "./container.js";
+import { position } from "./position.js";
+import { move_direction } from "./moving.js";
+
+class scene_ui {
+    #box;
+    #worker;
+    #last_mouse_position;
+    #step_left;
+    #step_right;
+    #step_front;
+    #step_back;
+    #rotate_clockwise;
+    #rotate_countclockwise;
+
+    constructor(worker) {
+        if (!(worker instanceof scene)) {
+            throw new TypeError("Worker must be instance of scene.");
+        }
+
+        this.#last_mouse_position = undefined;
+        this.#worker = worker;
+
+        this.#step_front = this.#create_push(
+            "front", "arrow_drop_up", 
+            (player) => { player.movement.y = move_direction.add; }
+        );
+        
+        this.#step_back = this.#create_push(
+            "back", "arrow_drop_down",
+            (player) => { player.movement.y = move_direction.sub; }
+        );
+
+        this.#step_left = this.#create_push(
+            "left", "arrow_left",
+            (player) => { player.movement.x = move_direction.sub; }
+        );
+
+        this.#step_right = this.#create_push(
+            "right", "arrow_right", 
+            (player) => { player.movement.x = move_direction.add; }
+        );
+
+        this.#rotate_clockwise = this.#create_push(
+            "clockwise", "rotate_right", 
+            (player) => { player.rotation.z = move_direction.add; }
+        );
+
+        this.#rotate_countclockwise = this.#create_push(
+            "countclockwise", "rotate_left", 
+            (player) => { player.rotation.z = move_direction.sub; }
+        );
+
+        this.#setup_stopers();
+        this.#setup_keybind();
+        this.#setup_mousebind();
+
+        this.#box = container("controls", (root) => {
+            root.appendChild(container("top", (top) => {
+                top.appendChild(this.#rotate_countclockwise);
+                top.appendChild(this.#step_front);
+                top.appendChild(this.#rotate_clockwise);
+            }));
+
+            root.appendChild(container("bottom", (bottom) => {
+                bottom.appendChild(this.#step_left);
+                bottom.appendChild(this.#step_back);
+                bottom.appendChild(this.#step_right);
+            }));
+        });
+    }
+
+    get box() {
+        return this.#box;
+    }
+
+    #setup_keybind() {
+        document.addEventListener("keydown", (action) => {
+            switch (action.key) {
+                case "w":
+                    this.#worker.player.movement.y = move_direction.add;
+                    break;
+                
+                case "s":
+                    this.#worker.player.movement.y = move_direction.sub;
+                    break;
+
+                case "d":
+                    this.#worker.player.movement.x = move_direction.add;
+                    break;
+
+                case "a":
+                    this.#worker.player.movement.x = move_direction.sub;
+                    break;
+
+                default:
+                    break;
+            }
+        });
+    
+        document.addEventListener("keyup", (action) => {
+            switch (action.key) {
+                case "w":
+                case "s":
+                    this.#worker.player.movement.y = move_direction.stop;
+                    break;
+
+                case "a":
+                case "d":
+                    this.#worker.player.movement.x = move_direction.stop;
+                    break;
+
+                default:
+                    break;
+            }
+        });
+    }
+
+    #setup_mousebind() {
+        document.addEventListener("mouseout", (action) => {
+            this.#last_mouse_position = undefined;
+        });
+
+        document.addEventListener("mousemove", (action) => {
+            const from_mouse = () => {
+                return new position(action.clientX, action.clientY);
+            };
+
+            if (this.#last_mouse_position === undefined) {
+                this.#last_mouse_position = from_mouse();
+                return;
+            }
+
+            const current = from_mouse();
+            const difference = current.compare(this.#last_mouse_position);
+            const size = new position(window.innerWidth, window.innerHeight);
+
+            this.#last_mouse_position = current;
+            this.#worker.player.rotate_cursor(difference, size);
+        });
+    }
+
+    #setup_stopers() {
+        const stoper = () => {
+            this.#worker.player.movement.stop();
+            this.#worker.player.rotation.stop();
+        };
+
+        document.addEventListener("mouseup", stoper);
+        document.addEventListener("mouseover", stoper);
+    }
+
+    #create_push(name, icon, move) {
+        return push(name, null, (target) => {
+            target.innerText = "";
+            target.appendChild(material_icon(icon));
+        
+            target.addEventListener("mousedown", () => {
+                move(this.#worker.player);
+            });
+        });
+    }
+}
+
+export { scene_ui };

+ 27 - 0
scripts/space/scene.js

@@ -0,0 +1,27 @@
+import { actor } from "./actor.js";
+import { render_engine } from "./render-engine.js"
+
+class scene extends render_engine {
+    constructor(initializer = null) {
+        if (initializer !== null && typeof(initializer) !== "function") {
+            throw new TypeError("Initializer or function.");
+        }
+
+        const player = new actor();
+        const canvas = document.createElement("canvas");
+        const context = canvas.getContext("webgl2");
+
+        if (!context) {
+            throw new TypeError("Browser does not support WebGL.");
+        }
+
+        canvas.classList.add("space-render");
+        super(canvas, context, player);
+
+        if (initializer) {
+            initializer(this);
+        }
+    }
+}
+
+export { scene };

+ 84 - 0
theme/icons/shelf.svg

@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="140.15248mm"
+   height="66.821518mm"
+   viewBox="0 0 140.15248 66.821518"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   sodipodi:docname="shelf.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     inkscape:zoom="1.4930696"
+     inkscape:cx="303.4018"
+     inkscape:cy="37.171744"
+     inkscape:window-width="1840"
+     inkscape:window-height="977"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="g6" />
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-26.784309,-60.268365)">
+    <g
+       id="g7">
+      <rect
+         style="fill:#6c6753;stroke:#000000;stroke-width:4.065;stroke-dasharray:none;stroke-opacity:1"
+         id="rect1"
+         width="136.08748"
+         height="15.043117"
+         x="28.816809"
+         y="110.01427"
+         ry="7.5215583"
+         rx="7.51829" />
+      <g
+         id="g6"
+         transform="translate(-2.3594971,-2.0048756)">
+        <rect
+           style="fill:#71c837;stroke:#000000;stroke-width:4.065;stroke-dasharray:none;stroke-opacity:1"
+           id="rect6"
+           width="31.576786"
+           height="30.824961"
+           x="48.367622"
+           y="74.368767"
+           ry="5.3880939"
+           rx="4.5109701" />
+        <rect
+           style="fill:#ff9955;stroke:#000000;stroke-width:4.065;stroke-dasharray:none;stroke-opacity:1"
+           id="rect6-5"
+           width="30.111774"
+           height="40.887985"
+           x="119.96069"
+           y="64.30574"
+           ry="4.3237448"
+           rx="4.3016825" />
+        <ellipse
+           style="fill:#8800aa;stroke:#000000;stroke-width:4.065;stroke-dasharray:none;stroke-opacity:1"
+           id="path6"
+           cx="99.952553"
+           cy="91.622421"
+           rx="12.278799"
+           ry="13.571304" />
+      </g>
+    </g>
+  </g>
+</svg>

+ 98 - 0
theme/icons/space.svg

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="106.66824mm"
+   height="53.151524mm"
+   viewBox="0 0 106.66824 53.151524"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   sodipodi:docname="space.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     inkscape:zoom="1.4930696"
+     inkscape:cx="214.65845"
+     inkscape:cy="101.13393"
+     inkscape:window-width="1920"
+     inkscape:window-height="1080"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs1">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="253.11552 : -80.061301 : 1"
+       inkscape:vp_y="0 : 999.99997 : 0"
+       inkscape:vp_z="-308.26031 : -83.820451 : 1"
+       inkscape:persp3d-origin="105 : -144.84848 : 1"
+       id="perspective8" />
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(-52.724651,-130.16439)">
+    <g
+       id="g8">
+      <rect
+         style="fill:#8800aa;stroke:#000000;stroke-width:4.06501;stroke-linecap:round;stroke-linejoin:round"
+         id="rect5"
+         width="102.60323"
+         height="49.086514"
+         x="54.757156"
+         y="132.1969"
+         rx="4.3016825"
+         ry="4.3237448" />
+      <g
+         id="g7"
+         transform="translate(2.1264916,-0.44301319)">
+        <circle
+           style="fill:#37c8ab;stroke:#000000;stroke-width:4.06501;stroke-linecap:round;stroke-linejoin:round"
+           id="path5"
+           cx="136.8929"
+           cy="149.29742"
+           r="10.366647" />
+        <g
+           id="g6">
+          <rect
+             style="fill:#aad400;stroke:#000000;stroke-width:4.06501;stroke-linecap:round;stroke-linejoin:round"
+             id="rect6"
+             width="15.483311"
+             height="9.5692129"
+             x="129.15125"
+             y="165.86635"
+             rx="4.3016825"
+             ry="4.3237448" />
+          <rect
+             style="fill:#aad400;stroke:#000000;stroke-width:4.06501;stroke-linecap:round;stroke-linejoin:round"
+             id="rect6-2"
+             width="15.483311"
+             height="9.5692129"
+             x="99.911133"
+             y="165.86635"
+             rx="4.3016825"
+             ry="4.3237448" />
+        </g>
+      </g>
+    </g>
+    <path
+       id="rect8"
+       style="fill:#00aa00;stroke:#000000;stroke-width:4.06501;stroke-linecap:round;stroke-linejoin:round"
+       d="m 77.369624,144.33549 c -2.383132,0 -4.301546,1.92841 -4.301546,4.32377 v 2.76469 h -2.764689 c -2.395355,0 -4.323767,1.91841 -4.323767,4.30154 v 2.02934 c 0,2.38313 1.928412,4.30154 4.323767,4.30154 h 2.764689 v 2.76469 c 0,2.39536 1.918414,4.32377 4.301546,4.32377 h 2.029334 c 2.383132,0 4.301546,-1.92841 4.301546,-4.32377 v -2.76469 h 2.764689 c 2.395355,0 4.323772,-1.91841 4.323772,-4.30154 v -2.02934 c 0,-2.38313 -1.928417,-4.30154 -4.323772,-4.30154 h -2.764689 v -2.76469 c 0,-2.39536 -1.918414,-4.32377 -4.301546,-4.32377 z" />
+  </g>
+</svg>

+ 0 - 0
theme/app.css → theme/style/app.css


+ 1 - 0
theme/colors.css → theme/style/colors.css

@@ -5,6 +5,7 @@ body {
     --font-color-light: #120309;
     --primary-color: #307351;
     --secondary-color: #53599A;
+    --icon-background-color: #E0E2DB;
 }
 
 .darkmode {

+ 0 - 0
theme/font.css → theme/style/font.css


+ 5 - 0
theme/style/icon.css

@@ -0,0 +1,5 @@
+img.icon {
+    background-color: var(--icon-background-color);
+    padding: 20px;
+    border-radius: 20px;
+}

+ 3 - 1
theme/loader.css → theme/style/loader.css

@@ -2,4 +2,6 @@
 @import url("./app.css");
 @import url("./font.css");
 @import url("./push.css");
-@import url("./scene.css");
+@import url("./scene.css");
+@import url("./icon.css");
+@import url("./mode-selector.css");

+ 53 - 0
theme/style/mode-selector.css

@@ -0,0 +1,53 @@
+.mode-selector {
+    box-sizing: border-box;
+    position: absolute;
+    top: 0px;
+    left: 0px;
+    width: 100vw;
+    height: 100vh;
+    padding-top: 40px;
+    padding-bottom: 40px;
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+    justify-content: center;
+    align-items: center;
+    gap: 40px;
+    overflow-y: auto;
+}
+
+.mode-selector .selector-item {
+    box-sizing: border-box;
+    width: 30%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    border-radius: 40px;
+    padding: 40px;
+    border: 3px solid transparent;
+    transition:
+        border-color 0.5s,
+        transform 0.5s;
+}
+
+@media only screen and (max-width: 800px) {
+    .mode-selector .selector-item {
+        width: 100%;
+        max-width: 400px;
+    }
+}
+
+.mode-selector .selector-item p {
+    font-size: 24px;
+    font-weight: bold;
+}
+
+.mode-selector .selector-item img {
+    width: 100%;
+    box-sizing: border-box;
+}
+
+.mode-selector .selector-item:hover {
+    border-color: var(--font-color);
+    transform: scale(1.1);
+}

+ 0 - 0
theme/push.css → theme/style/push.css


+ 0 - 0
theme/scene.css → theme/style/scene.css