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
|
# 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
11
.gitignore
vendored
@@ -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
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
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
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',
|
||||||
@@ -13,9 +15,8 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
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
1166
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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,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-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'),
|
||||||
|
local('#{$font-with-space} Italic'),
|
||||||
url('#{$font-with-path}Italic.eot?#iefix') format('embedded-opentype'),
|
url('#{$font-with-path}Italic.eot?#iefix') format('embedded-opentype'),
|
||||||
url('#{$font-with-path}Italic.woff2') format('woff2'),
|
url('#{$font-with-path}Italic.woff2') format('woff2'),
|
||||||
url('#{$font-with-path}Italic.woff') format('woff'),
|
url('#{$font-with-path}Italic.woff') format('woff'),
|
||||||
@@ -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')
|
||||||
|
format('embedded-opentype'),
|
||||||
url('@/assets/fonts/computer-modern/cmunrm.woff') format('woff'),
|
url('@/assets/fonts/computer-modern/cmunrm.woff') format('woff'),
|
||||||
url('@/assets/fonts/computer-modern/cmunrm.ttf') format('truetype');
|
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');
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ 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>
|
||||||
@@ -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
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">
|
<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>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<button v-if="!showMenu" class="button" @click="showMenu = true">
|
<button
|
||||||
|
v-if="!showMenu"
|
||||||
|
class="button button__show"
|
||||||
|
@click="showMenu = true"
|
||||||
|
>
|
||||||
<IconMenu />
|
<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%;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
|
|||||||
@@ -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
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