1
0
mirror of https://github.com/robonen/canvas-3d.git synced 2026-03-20 02:44:40 +00:00

Merge pull request #11 from robonen/engine

Engine v1
This commit is contained in:
2022-12-21 08:24:44 +07:00
committed by GitHub
31 changed files with 1261 additions and 673 deletions

View File

@@ -4,6 +4,7 @@
# Allow files and directories # Allow files and directories
!.env !.env
!src !src
!packages
!nuxt.config.ts !nuxt.config.ts
!tsconfig.json !tsconfig.json
!package.json !package.json

11
.gitignore vendored
View File

@@ -2,7 +2,6 @@
.vscode .vscode
.idea .idea
# Temporary files # Temporary files
node_modules node_modules
**/*~ **/*~
@@ -11,12 +10,10 @@ node_modules
**/Thumbs.db **/Thumbs.db
# Build # Build
**/.nuxt .nuxt
**/.nitro src/.nuxt
**/.cache output
**/.output dist
**/dist
# Environment # Environment
.env .env
.env

8
.prettierrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"tabWidth": 2,
"printWidth": 80,
"semi": true
}

View File

@@ -16,7 +16,6 @@ services:
- app - app
app: app:
# container_name: app
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -25,8 +24,6 @@ services:
replicas: 2 replicas: 2
expose: expose:
- '${FORWARD_APP_PORT:-3000}' - '${FORWARD_APP_PORT:-3000}'
# ports:
# - '${FORWARD_APP_PORT:-3000}:3000'
networks: networks:
- c3d_net - c3d_net
depends_on: depends_on:

View File

@@ -1,21 +1,22 @@
// https://v3.nuxtjs.org/api/configuration/nuxt.config // https://v3.nuxtjs.org/api/configuration/nuxt.config
import {resolve} from 'path'; import { resolve } from 'path';
const SRC = resolve(__dirname, 'src'); const SRC = resolve(__dirname, 'src');
const PACKAGES = resolve(__dirname, 'packages');
export default defineNuxtConfig({ export default defineNuxtConfig({
srcDir: SRC, srcDir: SRC,
ssr: false,
app: { app: {
head: { head: {
title: 'Canvas 3D', title: 'Canvas 3D',
link: [{rel: 'icon', href: '/favicon.svg'}], link: [{ rel: 'icon', href: '/favicon.svg' }],
}, },
}, },
css: ['@/assets/styles/main.scss'], css: ['@/assets/styles/main.scss'],
typescript: { typescript: {
typeCheck: true,
shim: false, shim: false,
}, },
modules: [ modules: ['@vueuse/nuxt'],
'@vueuse/nuxt', });
],
})

1166
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,8 +21,10 @@
"devDependencies": { "devDependencies": {
"@vueuse/core": "^9.3.1", "@vueuse/core": "^9.3.1",
"@vueuse/nuxt": "^9.3.1", "@vueuse/nuxt": "^9.3.1",
"nuxt": "^3.0.0",
"husky": "^8.0.2", "husky": "^8.0.2",
"sass": "^1.55.0" "nuxt": "^3.0.0",
"prettier": "^2.8.0",
"sass": "^1.55.0",
"vue-tsc": "^1.0.9"
} }
} }

View File

@@ -0,0 +1,7 @@
FROM ubuntu:22.04
MAINTAINER Robonen Andrew <robonenandrew@gmail.com>
WORKDIR /src
RUN apt update && apt install -y clang lldb lld

3
packages/matrix/build.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/usr/bin/env sh
docker build -t llvm .

7
packages/matrix/run.sh Normal file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env sh
docker run --rm \
-v $(pwd)/src:/src \
-v $(pwd)/dist:/dist \
llvm \
clang --target=wasm32 -O3 -fno-builtin -flto -nostdlib -Wl,--no-entry -Wl,--export-all -Wl,--lto-O3 -o /dist/matrix.wasm /src/matrix.c

View File

@@ -0,0 +1,22 @@
/*
* Matrix multiplication
* C = A * B
*/
void matrix_mul(double *A, double *B, double *C, unsigned m, unsigned n, unsigned p)
{
unsigned i, j, k;
double sum;
for (i = 0; i < m; i++)
{
for (j = 0; j < p; j++)
{
sum = 0;
for (k = 0; k < n; k++)
{
sum += A[i * n + k] * B[k * p + j];
}
C[i * p + j] = sum;
}
}
}

View File

View File

View File

@@ -0,0 +1,4 @@
export type Vec3 = [number, number, number];
export type Vec4 = [number, number, number, number];
export type Mat4 = [Vec4, Vec4, Vec4, Vec4];

View File

@@ -1,5 +1,5 @@
<template> <template>
<NuxtLayout> <NuxtLayout>
<NuxtPage/> <NuxtPage />
</NuxtLayout> </NuxtLayout>
</template> </template>

View File

@@ -1,12 +1 @@
.slide-leave-active,
.slide-enter-active {
transition: 1s;
}
.slide-enter {
transform: translate(100%, 0);
}
.slide-leave-to {
transform: translate(-100%, 0);
}

View File

@@ -8,10 +8,10 @@
font-family: $font-family; font-family: $font-family;
src: url('#{$font-with-path}.eot'); src: url('#{$font-with-path}.eot');
src: local('#{$font-with-dash}'), local('#{$font-with-space}'), src: local('#{$font-with-dash}'), local('#{$font-with-space}'),
url('#{$font-with-path}.eot?#iefix') format('embedded-opentype'), url('#{$font-with-path}.eot?#iefix') format('embedded-opentype'),
url('#{$font-with-path}.woff2') format('woff2'), url('#{$font-with-path}.woff2') format('woff2'),
url('#{$font-with-path}.woff') format('woff'), url('#{$font-with-path}.woff') format('woff'),
url('#{$font-with-path}.ttf') format('truetype'); url('#{$font-with-path}.ttf') format('truetype');
font-weight: $weight; font-weight: $weight;
font-style: normal; font-style: normal;
} }
@@ -19,11 +19,12 @@
@font-face { @font-face {
font-family: $font-family; font-family: $font-family;
src: url('#{$font-with-path}Italic.eot'); src: url('#{$font-with-path}Italic.eot');
src: local('#{$font-with-dash}Italic'), local('#{$font-with-space} Italic'), src: local('#{$font-with-dash}Italic'),
url('#{$font-with-path}Italic.eot?#iefix') format('embedded-opentype'), local('#{$font-with-space} Italic'),
url('#{$font-with-path}Italic.woff2') format('woff2'), url('#{$font-with-path}Italic.eot?#iefix') format('embedded-opentype'),
url('#{$font-with-path}Italic.woff') format('woff'), url('#{$font-with-path}Italic.woff2') format('woff2'),
url('#{$font-with-path}Italic.ttf') format('truetype'); url('#{$font-with-path}Italic.woff') format('woff'),
url('#{$font-with-path}Italic.ttf') format('truetype');
font-weight: $weight; font-weight: $weight;
font-style: italic; font-style: italic;
} }
@@ -38,14 +39,15 @@ $fonts: (
Black: 700, Black: 700,
); );
@include MakeFont('Formular', $fonts, '@/assets/fonts/formular');
@font-face { @font-face {
font-family: 'Computer Modern Serif'; font-family: 'Computer Modern Serif';
src: url('@/assets/fonts/computer-modern/cmunrm.eot'); src: url('@/assets/fonts/computer-modern/cmunrm.eot');
src: url('@/assets/fonts/computer-modern/cmunrm.eot?#iefix') format('embedded-opentype'), src: url('@/assets/fonts/computer-modern/cmunrm.eot?#iefix')
url('@/assets/fonts/computer-modern/cmunrm.woff') format('woff'), format('embedded-opentype'),
url('@/assets/fonts/computer-modern/cmunrm.ttf') format('truetype'); url('@/assets/fonts/computer-modern/cmunrm.woff') format('woff'),
url('@/assets/fonts/computer-modern/cmunrm.ttf') format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@include MakeFont('Formular', $fonts, '@/assets/fonts/formular');

View File

@@ -7,6 +7,6 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background-color: var(--scroll-color);
transition: background-color 0.1s; transition: background-color 0.1s;
background-color: var(--scroll-color);
} }

View File

@@ -4,15 +4,17 @@
@import 'scroll'; @import 'scroll';
@import 'animations'; @import 'animations';
html, body, #__nuxt { html,
body,
#__nuxt {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
body { body {
background-color: var(--background-color-primary);
color: var(--font-color-primary);
font-family: Formular, Helvetica, Arial, sans-serif; font-family: Formular, Helvetica, Arial, sans-serif;
color: var(--font-color-primary);
background-color: var(--background-color-primary);
} }
button { button {
@@ -21,7 +23,8 @@ button {
cursor: pointer; cursor: pointer;
} }
h1, h2 { h1,
h2 {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
} }

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const {title} = defineProps<{ title: string }>(); const { title } = defineProps<{ title: string }>();
const showForm = ref<boolean>(false); const showForm = ref<boolean>(false);
</script> </script>
@@ -8,12 +8,11 @@ const showForm = ref<boolean>(false);
<div class="header" @click="showForm = !showForm"> <div class="header" @click="showForm = !showForm">
<h2>{{ title }}</h2> <h2>{{ title }}</h2>
<button class="button"> <button class="button">
<IconClose v-if="showForm"/> <IconOpen :class="{ icon_close: showForm }" class="icon" />
<IconOpen v-else/>
</button> </button>
</div> </div>
<div class="content" v-if="showForm"> <div class="content" v-show="showForm">
<slot/> <slot />
</div> </div>
</section> </section>
</template> </template>
@@ -27,14 +26,22 @@ const showForm = ref<boolean>(false);
.header { .header {
display: flex; display: flex;
justify-content: space-between;
align-items: center; align-items: center;
justify-content: space-between;
column-gap: 12px; column-gap: 12px;
user-select: none; user-select: none;
cursor: pointer; cursor: pointer;
padding: 8px 0; padding: 8px 0;
} }
.icon {
transition: transform 0.2s;
&_close {
transform: rotate(180deg);
}
}
.button { .button {
color: var(--icon-color); color: var(--icon-color);
} }

323
src/components/board.vue Normal file
View File

@@ -0,0 +1,323 @@
<script setup lang="ts">
const canvas = ref<HTMLCanvasElement | null>(null);
onMounted(() => {
if (!canvas.value) return;
canvas.value.width = window.innerWidth;
canvas.value.height = window.innerHeight;
const ctx = canvas.value.getContext('2d');
if (!ctx) return;
ctx.fillStyle = 'red';
type Point = [number, number, number, number];
const sizeX = canvas.value.width;
const sizeY = canvas.value.height;
const centerX = sizeX / 2;
const centerY = sizeY / 2;
const figureSize = 200;
const figureList = {
[Figures.CUBE]: {
points: [
[0, 0, 0, 1],
[0, 0, 200, 1],
[0, 200, 0, 1],
[0, 200, 200, 1],
[200, 0, 0, 1],
[200, 0, 200, 1],
[200, 200, 0, 1],
[200, 200, 200, 1],
] as Point[],
faces: [
[0, 1, 3, 2],
[0, 1, 5, 4],
[0, 2, 6, 4],
[1, 3, 7, 5],
[2, 3, 7, 6],
[4, 5, 7, 6],
],
},
[Figures.OCTAHEDRON]: {
points: [
[0, 0, 100, 1],
[100, 100, 0, 1],
[100, -100, 0, 1],
[-100, -100, 0, 1],
[-100, 100, 0, 1],
[0, 0, -100, 1],
] as Point[],
faces: [
[0, 1, 2],
[0, 2, 3],
[0, 3, 4],
[0, 4, 1],
[5, 1, 2],
[5, 2, 3],
[5, 3, 4],
[5, 4, 1],
],
},
[Figures.TRIHEDRAL_PYRAMID]: {
points: [
[0, 0, 100, 1],
[0, 80, 0, 1],
[86.6, -50, 0, 1],
[-86.6, -50, 0, 1],
] as Point[],
faces: [
[0, 1, 2],
[0, 2, 3],
[0, 3, 1],
[1, 2, 3],
],
},
[Figures.SQUARE_PYRAMID]: {
points: [
[0, 0, figureSize, 1],
[100, 100, 0, 1],
[100, -100, 0, 1],
[-100, -100, 0, 1],
[-100, 100, 0, 1],
] as Point[],
faces: [
[0, 1, 2],
[0, 2, 3],
[0, 3, 4],
[0, 4, 1],
[1, 2, 3, 4],
],
},
[Figures.PENTAGONAL_PYRAMID]: {
points: [
[0, 0, figureSize, 1],
[0, 100, 0, 1],
[95.1, 30.9, 0, 1],
[58.8, -80.9, 0, 1],
[-58.8, -80.9, 0, 1],
[-95.1, 30.9, 0, 1],
] as Point[],
faces: [
[0, 1, 2],
[0, 2, 3],
[0, 3, 4],
[0, 4, 5],
[0, 5, 1],
[1, 2, 3, 4, 5],
],
},
};
const identityMatrix: Point[] = [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
];
const translationMatrix: Point[] = [
[1, 0, 0, 0],
[0, -1, 0, 0],
[0, 0, 1, 0],
[centerX, centerY, 0, 1],
];
// Rotate around X axis
const rotateX = (angle: number): Point[] => {
const rad = (angle * Math.PI) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
return [
[1, 0, 0, 0],
[0, cos, -sin, 0],
[0, sin, cos, 0],
[0, 0, 0, 1],
];
};
// Rotate around Y axis
const rotateY = (angle: number): Point[] => {
const rad = (angle * Math.PI) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
return [
[cos, 0, sin, 0],
[0, 1, 0, 0],
[-sin, 0, cos, 0],
[0, 0, 0, 1],
];
};
// Rotate around Z axis
const rotateZ = (angle: number): Point[] => {
const rad = (angle * Math.PI) / 180;
const cos = Math.cos(rad);
const sin = Math.sin(rad);
return [
[cos, -sin, 0, 0],
[sin, cos, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1],
];
};
const rotate = (x: number, y: number, z: number): Point[] => {
return mul([rotateX(x), rotateY(y), rotateZ(z)]);
};
// Translate
const translate = (x: number, y: number, z: number): Point[] => {
return [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[x, y, z, 1],
];
};
// Scale
const scaleMatrix = (x: number, y: number, z: number): Point[] => {
return [
[x, 0, 0, 0],
[0, y, 0, 0],
[0, 0, z, 0],
[0, 0, 0, 1],
];
};
const projections = {
[Projections.NONE]: (): Point[] => identityMatrix,
[Projections.ISOMETRIC]: (): Point[] => [
[0.707, -0.408, 0, 0],
[0, 0.816, 0, 0],
[-0.707, -0.408, 1, 0],
[0, 0, 0, 1],
],
[Projections.DIMETRIC]: (): Point[] => [
[0.926, 0.134, 0, 0],
[0, 0.935, 0, 0],
[0.378, -0.327, 0, 0],
[0, 0, 0, 1],
],
[Projections.TRIMETRIC]: (): Point[] => [
[0.866, 0.354, 0, 0],
[0, 0.707, 0, 0],
[0.5, -0.612, 0, 0],
[0, 0, 0, 1],
],
[Projections.ONE_POINT_PERSPECTIVE]: (): Point[] => [
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 0.001],
[0, 0, 0, 1],
],
[Projections.TWO_POINT_PERSPECTIVE]: (): Point[] => [
[1, 0, 0, 0.001],
[0, 1, 0, 0.001],
[0, 0, 0, 0],
[0, 0, 0, 1],
],
};
// Multiply array of points by matrix
const multiply = (points: Point[], matrix: Point[]): Point[] => {
const result: Point[] = [];
for (const point of points) {
const [x, y, z, w] = point;
const [x1, y1, z1, w1] = matrix[0];
const [x2, y2, z2, w2] = matrix[1];
const [x3, y3, z3, w3] = matrix[2];
const [x4, y4, z4, w4] = matrix[3];
result.push([
x * x1 + y * x2 + z * x3 + w * x4,
x * y1 + y * y2 + z * y3 + w * y4,
x * z1 + y * z2 + z * z3 + w * z4,
x * w1 + y * w2 + z * w3 + w * w4,
]);
}
return result;
};
const mul = (matrices: Point[][]) => {
let result = matrices[0];
for (let i = 1; i < matrices.length; i++) {
result = multiply(result, matrices[i]);
}
return result;
};
// Draw figure
const drawFigure = (points: Point[], faces: number[][]) => {
ctx.clearRect(0, 0, sizeX, sizeY);
for (const face of faces) {
ctx.beginPath();
for (let i = 0; i < face.length; i++) {
const [x, y] = points[face[i]];
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.stroke();
}
};
const { currentFigure } = useFigure();
useTransformations((translation, rotation, scale, prj) => {
const matrix = mul([
figureList[currentFigure.value].points,
scaleMatrix(scale[0], scale[1], scale[2]),
rotate(rotation[0], rotation[1], rotation[2]),
translate(translation[0], translation[1], translation[2]),
projections[prj](),
]);
for (let i = 0; i < matrix.length; i++) {
matrix[i][0] = matrix[i][0] / matrix[i][3];
matrix[i][1] = matrix[i][1] / matrix[i][3];
matrix[i][2] = matrix[i][2] / matrix[i][3];
matrix[i][3] = 1;
}
drawFigure(
mul([matrix, translationMatrix]),
figureList[currentFigure.value].faces
);
});
});
</script>
<template>
<canvas ref="canvas"> Sorry, your browser doesn't support canvas. </canvas>
</template>

View File

@@ -1,35 +1,65 @@
<script setup lang="ts"> <script setup lang="ts">
import { HTMLElementEvent } from '@/types/dom';
const { const {
label, label,
min, min,
max, max,
currentValue,
defaultValue, defaultValue,
step = 0.1 step = 0.1,
} = defineProps<{ label: string, min: number, max: number, defaultValue: number, step?: number }>(); } = defineProps<{
label: string;
const emit = defineEmits<{ min: number;
(event: 'change', value: number): void max: number;
currentValue: number;
defaultValue: number;
step?: number;
}>(); }>();
const value = ref<number>(defaultValue); const emit = defineEmits<{
(event: 'change', value: number): void;
}>();
const value = ref<number>(currentValue);
const onChange = (event: Event) => { const onChange = (event: Event) => {
value.value = (event.target as HTMLInputElement).valueAsNumber; const { target } = event as HTMLElementEvent<HTMLInputElement>;
value.value = target.valueAsNumber;
emit('change', value.value); emit('change', value.value);
}; };
const reset = () => {
value.value = defaultValue;
emit('change', value.value);
};
</script> </script>
<template> <template>
<div class="block"> <div class="block">
<label class="label"> <label class="label">
{{ label }} {{ label }}
<input :max="max" :min="min" :value="value" class="input" type="number" @input="onChange"/> <input
:max="max"
:min="min"
:value="value"
class="input"
type="number"
@input="onChange"
/>
</label> </label>
<div class="range"> <div class="range">
<div class="range__border">{{ min }}</div> <div class="range__border">{{ min }}</div>
<input :max="max" :min="min" :step="step" :value="value" class="range__input" type="range" @input="onChange" <input
@dblclick="value = defaultValue"/> :max="max"
:min="min"
:step="step"
:value="value"
class="range__input"
type="range"
@input="onChange"
@click.middle="reset"
/>
<div class="range__border">{{ max }}</div> <div class="range__border">{{ max }}</div>
</div> </div>
</div> </div>
@@ -60,7 +90,7 @@ const onChange = (event: Event) => {
margin: 0; margin: 0;
} }
&[type=number] { &[type='number'] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }
} }
@@ -84,9 +114,10 @@ const onChange = (event: Event) => {
border-radius: 4px; border-radius: 4px;
margin: 10px 0; margin: 10px 0;
background: #e2e4e6; background: #e2e4e6;
transition: opacity .2s; transition: opacity 0.2s;
&:hover, &:focus { &:hover,
&:focus {
opacity: 1; opacity: 1;
} }
@@ -100,6 +131,4 @@ const onChange = (event: Event) => {
cursor: pointer; cursor: pointer;
} }
} }
</style> </style>

View File

@@ -1,14 +1,14 @@
<template> <template>
<div class="grid"> <div class="grid">
<slot/> <slot />
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.grid { .grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr);
grid-column-gap: 16px; grid-column-gap: 16px;
grid-row-gap: 16px; grid-row-gap: 16px;
grid-template-columns: repeat(2, 1fr);
} }
</style> </style>

View File

@@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
const {title, isActive = false} = defineProps<{ title?: string, isActive?: boolean }>(); const { title, isActive = false } = defineProps<{
title?: string;
isActive?: boolean;
}>();
</script> </script>
<template> <template>
<button class="block" :class="{'block_active': isActive}"> <button class="block" :class="{ block_active: isActive }">
<div class="picture"></div> <div class="picture"></div>
<div v-if="title" class="title">{{ title }}</div> <div v-if="title" class="title">{{ title }}</div>
</button> </button>
@@ -23,7 +25,6 @@ const {title, isActive = false} = defineProps<{ title?: string, isActive?: boole
height: 200px; height: 200px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
box-sizing: border-box;
border-radius: 8px; border-radius: 8px;
background-color: #fae9ef; background-color: #fae9ef;
color: #67122c; color: #67122c;
@@ -54,8 +55,8 @@ const {title, isActive = false} = defineProps<{ title?: string, isActive?: boole
} }
.title { .title {
margin-top: 8px;
width: 100%; width: 100%;
margin-top: 8px;
text-align: left; text-align: left;
} }
</style> </style>

View File

@@ -0,0 +1,20 @@
export const enum Figures {
CUBE,
OCTAHEDRON,
TRIHEDRAL_PYRAMID,
SQUARE_PYRAMID,
PENTAGONAL_PYRAMID,
}
const currentFigure = ref<Figures>(Figures.PENTAGONAL_PYRAMID);
export const useFigure = () => {
const setFigure = (figure: Figures) => {
currentFigure.value = figure;
};
return {
currentFigure,
setFigure,
};
};

View File

@@ -0,0 +1,57 @@
export const enum Projections {
NONE,
ISOMETRIC,
DIMETRIC,
TRIMETRIC,
ONE_POINT_PERSPECTIVE,
TWO_POINT_PERSPECTIVE,
}
export type XYZ = [number, number, number];
type fn =
| ((
translation: XYZ,
rotation: XYZ,
scale: XYZ,
projection: Projections
) => void)
| null;
const translation = reactive<XYZ>([0, 0, 0]);
const rotation = reactive<XYZ>([0, 0, 0]);
const scale = reactive<XYZ>([1, 1, 1]);
const projection = ref<Projections>(Projections.NONE);
const setTranslation = (axis: number, value: number) => {
translation[axis] = value;
};
const setRotation = (axis: number, value: number) => {
rotation[axis] = value;
};
const setScale = (axis: number, value: number) => {
scale[axis] = value;
};
const setProjection = (value: Projections) => {
projection.value = value;
};
export const useTransformations = (onUpdate: fn = null) => {
watchEffect(() => {
if (onUpdate) onUpdate(translation, rotation, scale, projection.value);
});
return {
translation,
rotation,
scale,
projection,
setTranslation,
setRotation,
setScale,
setProjection,
};
};

View File

@@ -5,19 +5,23 @@ const showMenu = ref<boolean>(true);
<template> <template>
<div class="wrapper"> <div class="wrapper">
<div class="content"> <div class="content">
<button v-if="!showMenu" class="button" @click="showMenu = true"> <button
<IconMenu/> v-if="!showMenu"
class="button button__show"
@click="showMenu = true"
>
<IconMenu />
</button> </button>
<template v-else> <template v-else>
<div class="controls"> <div class="controls">
<slot/> <slot />
</div> </div>
<button class="button" @click="showMenu = false"> <button class="button button__hide" @click="showMenu = false">
<IconHide/> <IconHide />
</button> </button>
</template> </template>
</div> </div>
<canvas class="canvas"/> <Board class="canvas" />
</div> </div>
</template> </template>
@@ -40,7 +44,6 @@ const showMenu = ref<boolean>(true);
.controls { .controls {
position: relative; position: relative;
box-sizing: border-box;
padding: 28px 24px; padding: 28px 24px;
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
@@ -60,6 +63,16 @@ const showMenu = ref<boolean>(true);
z-index: 3; z-index: 3;
} }
.button__show {
width: 25px;
height: 25px;
}
.button__hide {
width: 22px;
height: 22px;
}
.canvas { .canvas {
position: absolute; position: absolute;
width: 100%; width: 100%;

View File

@@ -1,33 +1,80 @@
<script setup lang="ts"> <script setup lang="ts">
const axes = ['x', 'y', 'z']; const axes = ['x', 'y', 'z'];
const projections = [ const projections = [
'Без проекции', { projection: Projections.NONE, title: 'Без проекции' },
'Изометрическая', { projection: Projections.ISOMETRIC, title: 'Изометрическая' },
'Диметрическая', { projection: Projections.DIMETRIC, title: 'Диметрическая' },
'Триметрическая', { projection: Projections.TRIMETRIC, title: 'Триметрическая' },
'Одноточечная перспективная', {
'Двухточечная перспективная', projection: Projections.ONE_POINT_PERSPECTIVE,
title: 'Одноточечная перспектива',
},
{
projection: Projections.TWO_POINT_PERSPECTIVE,
title: 'Двухточечная перспектива',
},
]; ];
const activeProjection = ref<number>(0); const activeProjection = ref<number>(0);
const transformations = useTransformations();
onMounted(() => (activeProjection.value = transformations.projection.value));
const setActiveProjection = (index: number) => {
activeProjection.value = index;
transformations.setProjection(projections[index].projection);
};
</script> </script>
<template> <template>
<div class="container"> <div class="container">
<Accordion title="Перемещение"> <Accordion title="Перемещение">
<FormRange v-for="axis in axes" :label="`${axis} =`" :min="-10" :max="10" :step="0.1" :defaultValue="0"/> <FormRange
v-for="(axis, i) in axes"
:label="`${axis} =`"
:min="-500"
:max="500"
:step="5"
:current-value="transformations.translation[i]"
:defaultValue="0"
@change="transformations.setTranslation(i, $event)"
/>
</Accordion> </Accordion>
<Accordion title="Вращение"> <Accordion title="Вращение">
<FormRange v-for="axis in axes" :label="`${axis} =`" :min="0" :max="359" :step="1" :defaultValue="0"/> <FormRange
v-for="(axis, i) in axes"
:label="`${axis} =`"
:min="0"
:max="359"
:step="1"
:current-value="transformations.rotation[i]"
:defaultValue="0"
@change="transformations.setRotation(i, $event)"
/>
</Accordion> </Accordion>
<Accordion title="Масштабирование"> <Accordion title="Масштабирование">
<FormRange v-for="axis in axes" :key="axis" :label="`${axis} =`" :min="0.1" :max="5" :step="0.1" <FormRange
:defaultValue="1"/> v-for="(axis, i) in axes"
:key="axis"
:label="`${axis} =`"
:min="0.1"
:max="5"
:step="0.1"
:current-value="transformations.scale[i]"
:defaultValue="1"
@change="transformations.setScale(i, $event)"
/>
</Accordion> </Accordion>
<Accordion title="Проекции"> <Accordion title="Проекции">
<GridContainer> <GridContainer>
<GridElement v-for="(projection, i) in projections" :is-active="activeProjection === i" :key="projection" <GridElement
:title="projection" @click="activeProjection = i"/> v-for="(projection, i) in projections"
:key="projection.title"
:is-active="activeProjection === i"
:title="projection.title"
@click="setActiveProjection(i)"
/>
</GridContainer> </GridContainer>
</Accordion> </Accordion>
</div> </div>

View File

@@ -1,13 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
const figures = ['Тетраэдр', 'Гексаэдр', 'Октаэдр', 'Додекаэдр', 'Икосаэдр']; const allFigures = [
{ figure: Figures.CUBE, title: 'Куб' },
{ figure: Figures.OCTAHEDRON, title: 'Октаэдр' },
{ figure: Figures.TRIHEDRAL_PYRAMID, title: 'Трехгранная пирамида' },
{ figure: Figures.SQUARE_PYRAMID, title: 'Четырехгранная пирамида' },
{ figure: Figures.PENTAGONAL_PYRAMID, title: 'Пятигранная пирамиида' },
];
const activeFigure = ref<number>(0);
const figure = useFigure();
onMounted(() => (activeFigure.value = figure.currentFigure.value));
const selectFigure = (index: number) => {
activeFigure.value = index;
figure.setFigure(allFigures[index].figure);
navigateTo('/figure');
};
</script> </script>
<template> <template>
<h1>Выберете фигуру</h1> <h1>Выберете фигуру</h1>
<GridContainer> <GridContainer>
<GridElement v-for="figure in figures" :key="figure" :title="figure"/> <GridElement
v-for="(figure, i) in allFigures"
:key="figure.title"
:title="figure.title"
:is-active="activeFigure === i"
@click="selectFigure(i)"
/>
</GridContainer> </GridContainer>
</template> </template>
<style scoped lang="scss">
</style>

View File

@@ -1,6 +1,4 @@
export default defineEventHandler(() => { export default defineEventHandler(() => {
console.log('Request received');
return { return {
api: 'works', api: 'works',
}; };

3
src/types/dom.ts Normal file
View File

@@ -0,0 +1,3 @@
export type HTMLElementEvent<T extends HTMLElement> = Event & {
target: T;
}