mirror of
https://github.com/robonen/canvas-3d.git
synced 2026-03-20 02:44:40 +00:00
@@ -4,6 +4,7 @@
|
||||
# Allow files and directories
|
||||
!.env
|
||||
!src
|
||||
!packages
|
||||
!nuxt.config.ts
|
||||
!tsconfig.json
|
||||
!package.json
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -2,7 +2,6 @@
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
|
||||
# Temporary files
|
||||
node_modules
|
||||
**/*~
|
||||
@@ -11,12 +10,10 @@ node_modules
|
||||
**/Thumbs.db
|
||||
|
||||
# Build
|
||||
**/.nuxt
|
||||
**/.nitro
|
||||
**/.cache
|
||||
**/.output
|
||||
**/dist
|
||||
.nuxt
|
||||
src/.nuxt
|
||||
output
|
||||
dist
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env
|
||||
|
||||
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 80,
|
||||
"semi": true
|
||||
}
|
||||
@@ -16,7 +16,6 @@ services:
|
||||
- app
|
||||
|
||||
app:
|
||||
# container_name: app
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
@@ -25,8 +24,6 @@ services:
|
||||
replicas: 2
|
||||
expose:
|
||||
- '${FORWARD_APP_PORT:-3000}'
|
||||
# ports:
|
||||
# - '${FORWARD_APP_PORT:-3000}:3000'
|
||||
networks:
|
||||
- c3d_net
|
||||
depends_on:
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||
import {resolve} from 'path';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const SRC = resolve(__dirname, 'src');
|
||||
const PACKAGES = resolve(__dirname, 'packages');
|
||||
|
||||
export default defineNuxtConfig({
|
||||
srcDir: SRC,
|
||||
ssr: false,
|
||||
app: {
|
||||
head: {
|
||||
title: 'Canvas 3D',
|
||||
link: [{rel: 'icon', href: '/favicon.svg'}],
|
||||
link: [{ rel: 'icon', href: '/favicon.svg' }],
|
||||
},
|
||||
},
|
||||
css: ['@/assets/styles/main.scss'],
|
||||
typescript: {
|
||||
typeCheck: true,
|
||||
shim: false,
|
||||
},
|
||||
modules: [
|
||||
'@vueuse/nuxt',
|
||||
],
|
||||
})
|
||||
modules: ['@vueuse/nuxt'],
|
||||
});
|
||||
|
||||
1166
package-lock.json
generated
1166
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,8 +21,10 @@
|
||||
"devDependencies": {
|
||||
"@vueuse/core": "^9.3.1",
|
||||
"@vueuse/nuxt": "^9.3.1",
|
||||
"nuxt": "^3.0.0",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
7
packages/matrix/Dockerfile
Normal file
7
packages/matrix/Dockerfile
Normal 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
3
packages/matrix/build.sh
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
docker build -t llvm .
|
||||
7
packages/matrix/run.sh
Normal file
7
packages/matrix/run.sh
Normal 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
|
||||
22
packages/matrix/src/matrix.c
Normal file
22
packages/matrix/src/matrix.c
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
0
packages/rendering/camera.ts
Normal file
0
packages/rendering/camera.ts
Normal file
0
packages/rendering/index.ts
Normal file
0
packages/rendering/index.ts
Normal file
4
packages/rendering/types.ts
Normal file
4
packages/rendering/types.ts
Normal 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];
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage/>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
.slide-leave-active,
|
||||
.slide-enter-active {
|
||||
transition: 1s;
|
||||
}
|
||||
|
||||
.slide-enter {
|
||||
transform: translate(100%, 0);
|
||||
}
|
||||
|
||||
.slide-leave-to {
|
||||
transform: translate(-100%, 0);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
@font-face {
|
||||
font-family: $font-family;
|
||||
src: url('#{$font-with-path}Italic.eot');
|
||||
src: local('#{$font-with-dash}Italic'), local('#{$font-with-space} Italic'),
|
||||
src: local('#{$font-with-dash}Italic'),
|
||||
local('#{$font-with-space} Italic'),
|
||||
url('#{$font-with-path}Italic.eot?#iefix') format('embedded-opentype'),
|
||||
url('#{$font-with-path}Italic.woff2') format('woff2'),
|
||||
url('#{$font-with-path}Italic.woff') format('woff'),
|
||||
@@ -38,14 +39,15 @@ $fonts: (
|
||||
Black: 700,
|
||||
);
|
||||
|
||||
@include MakeFont('Formular', $fonts, '@/assets/fonts/formular');
|
||||
|
||||
@font-face {
|
||||
font-family: 'Computer Modern Serif';
|
||||
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')
|
||||
format('embedded-opentype'),
|
||||
url('@/assets/fonts/computer-modern/cmunrm.woff') format('woff'),
|
||||
url('@/assets/fonts/computer-modern/cmunrm.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@include MakeFont('Formular', $fonts, '@/assets/fonts/formular');
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: var(--scroll-color);
|
||||
transition: background-color 0.1s;
|
||||
background-color: var(--scroll-color);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
@import 'scroll';
|
||||
@import 'animations';
|
||||
|
||||
html, body, #__nuxt {
|
||||
html,
|
||||
body,
|
||||
#__nuxt {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color-primary);
|
||||
color: var(--font-color-primary);
|
||||
font-family: Formular, Helvetica, Arial, sans-serif;
|
||||
color: var(--font-color-primary);
|
||||
background-color: var(--background-color-primary);
|
||||
}
|
||||
|
||||
button {
|
||||
@@ -21,7 +23,8 @@ button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
h1,
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
const {title} = defineProps<{ title: string }>();
|
||||
const { title } = defineProps<{ title: string }>();
|
||||
const showForm = ref<boolean>(false);
|
||||
</script>
|
||||
|
||||
@@ -8,12 +8,11 @@ const showForm = ref<boolean>(false);
|
||||
<div class="header" @click="showForm = !showForm">
|
||||
<h2>{{ title }}</h2>
|
||||
<button class="button">
|
||||
<IconClose v-if="showForm"/>
|
||||
<IconOpen v-else/>
|
||||
<IconOpen :class="{ icon_close: showForm }" class="icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="content" v-if="showForm">
|
||||
<slot/>
|
||||
<div class="content" v-show="showForm">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -27,14 +26,22 @@ const showForm = ref<boolean>(false);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
column-gap: 12px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
transition: transform 0.2s;
|
||||
|
||||
&_close {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
color: var(--icon-color);
|
||||
}
|
||||
|
||||
323
src/components/board.vue
Normal file
323
src/components/board.vue
Normal 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>
|
||||
@@ -1,35 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { HTMLElementEvent } from '@/types/dom';
|
||||
|
||||
const {
|
||||
label,
|
||||
min,
|
||||
max,
|
||||
currentValue,
|
||||
defaultValue,
|
||||
step = 0.1
|
||||
} = defineProps<{ label: string, min: number, max: number, defaultValue: number, step?: number }>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'change', value: number): void
|
||||
step = 0.1,
|
||||
} = defineProps<{
|
||||
label: string;
|
||||
min: number;
|
||||
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) => {
|
||||
value.value = (event.target as HTMLInputElement).valueAsNumber;
|
||||
const { target } = event as HTMLElementEvent<HTMLInputElement>;
|
||||
value.value = target.valueAsNumber;
|
||||
emit('change', value.value);
|
||||
};
|
||||
|
||||
const reset = () => {
|
||||
value.value = defaultValue;
|
||||
emit('change', value.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="block">
|
||||
<label class="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>
|
||||
<div class="range">
|
||||
<div class="range__border">{{ min }}</div>
|
||||
<input :max="max" :min="min" :step="step" :value="value" class="range__input" type="range" @input="onChange"
|
||||
@dblclick="value = defaultValue"/>
|
||||
<input
|
||||
: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>
|
||||
</div>
|
||||
@@ -60,7 +90,7 @@ const onChange = (event: Event) => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&[type=number] {
|
||||
&[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
}
|
||||
@@ -84,9 +114,10 @@ const onChange = (event: Event) => {
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
background: #e2e4e6;
|
||||
transition: opacity .2s;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover, &:focus {
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -100,6 +131,4 @@ const onChange = (event: Event) => {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="grid">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-column-gap: 16px;
|
||||
grid-row-gap: 16px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
const {title, isActive = false} = defineProps<{ title?: string, isActive?: boolean }>();
|
||||
|
||||
const { title, isActive = false } = defineProps<{
|
||||
title?: string;
|
||||
isActive?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="block" :class="{'block_active': isActive}">
|
||||
<button class="block" :class="{ block_active: isActive }">
|
||||
<div class="picture"></div>
|
||||
<div v-if="title" class="title">{{ title }}</div>
|
||||
</button>
|
||||
@@ -23,7 +25,6 @@ const {title, isActive = false} = defineProps<{ title?: string, isActive?: boole
|
||||
height: 200px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8px;
|
||||
background-color: #fae9ef;
|
||||
color: #67122c;
|
||||
@@ -54,8 +55,8 @@ const {title, isActive = false} = defineProps<{ title?: string, isActive?: boole
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
20
src/composables/useFigure.ts
Normal file
20
src/composables/useFigure.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
57
src/composables/useTransformations.ts
Normal file
57
src/composables/useTransformations.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -5,19 +5,23 @@ const showMenu = ref<boolean>(true);
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="content">
|
||||
<button v-if="!showMenu" class="button" @click="showMenu = true">
|
||||
<IconMenu/>
|
||||
<button
|
||||
v-if="!showMenu"
|
||||
class="button button__show"
|
||||
@click="showMenu = true"
|
||||
>
|
||||
<IconMenu />
|
||||
</button>
|
||||
<template v-else>
|
||||
<div class="controls">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
<button class="button" @click="showMenu = false">
|
||||
<IconHide/>
|
||||
<button class="button button__hide" @click="showMenu = false">
|
||||
<IconHide />
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<canvas class="canvas"/>
|
||||
<Board class="canvas" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -40,7 +44,6 @@ const showMenu = ref<boolean>(true);
|
||||
|
||||
.controls {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 28px 24px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
@@ -60,6 +63,16 @@ const showMenu = ref<boolean>(true);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.button__show {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.button__hide {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
|
||||
@@ -1,33 +1,80 @@
|
||||
<script setup lang="ts">
|
||||
const axes = ['x', 'y', 'z'];
|
||||
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 transformations = useTransformations();
|
||||
|
||||
onMounted(() => (activeProjection.value = transformations.projection.value));
|
||||
|
||||
const setActiveProjection = (index: number) => {
|
||||
activeProjection.value = index;
|
||||
transformations.setProjection(projections[index].projection);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container">
|
||||
<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 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 title="Масштабирование">
|
||||
<FormRange v-for="axis in axes" :key="axis" :label="`${axis} =`" :min="0.1" :max="5" :step="0.1"
|
||||
:defaultValue="1"/>
|
||||
<FormRange
|
||||
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 title="Проекции">
|
||||
<GridContainer>
|
||||
<GridElement v-for="(projection, i) in projections" :is-active="activeProjection === i" :key="projection"
|
||||
:title="projection" @click="activeProjection = i"/>
|
||||
<GridElement
|
||||
v-for="(projection, i) in projections"
|
||||
:key="projection.title"
|
||||
:is-active="activeProjection === i"
|
||||
:title="projection.title"
|
||||
@click="setActiveProjection(i)"
|
||||
/>
|
||||
</GridContainer>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
<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>
|
||||
|
||||
<template>
|
||||
<h1>Выберете фигуру</h1>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export default defineEventHandler(() => {
|
||||
console.log('Request received');
|
||||
|
||||
return {
|
||||
api: 'works',
|
||||
};
|
||||
|
||||
3
src/types/dom.ts
Normal file
3
src/types/dom.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type HTMLElementEvent<T extends HTMLElement> = Event & {
|
||||
target: T;
|
||||
}
|
||||
Reference in New Issue
Block a user