Compare commits

...

10 commits

19 changed files with 1148 additions and 27 deletions

View file

@ -1,8 +1,15 @@
when:
- branch: main
event: push
- event: tag
steps:
- name: techtransthai-simple-deploy
image: docker.io/library/node:alpine
- name: deploy
image: node
commands:
- npm i
- node_modules/.bin/vite build --emptyOutDir --outDir /mnt/caddy-sites/fsob.techtransthai.org/
- npm run build
- rm -rf /mnt/caddy-sites/fsob.techtransthai.org/*
- cp -r dist/* /mnt/caddy-sites/fsob.techtransthai.org/
volumes:
- /media/core/Data1/Apps/caddy/sites:/mnt/caddy-sites

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
<title>Fifty Shades of Bully</title>
</head>
<body>
<div id="root"></div>

177
package-lock.json generated
View file

@ -8,8 +8,11 @@
"name": "fifty-shades-of-bully",
"version": "0.0.0",
"dependencies": {
"animejs": "^3.2.2",
"jszip": "^3.10.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
"@eslint/js": "^9.13.0",
@ -1264,6 +1267,12 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@ -1366,6 +1375,12 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/animejs": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/animejs/-/animejs-3.2.2.tgz",
"integrity": "sha512-Ao95qWLpDPXXM+WrmwcKbl6uNlC5tjnowlaRYtuVDHHoygjtIPfDUoK9NthrlZsQSKjZXlmji2TrBUAVbiH0LQ==",
"license": "MIT"
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -1695,6 +1710,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -2629,6 +2659,12 @@
"node": ">= 4"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -2656,6 +2692,12 @@
"node": ">=0.8.19"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/internal-slot": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
@ -3139,6 +3181,18 @@
"node": ">=4.0"
}
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -3163,6 +3217,15 @@
"node": ">= 0.8.0"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@ -3415,6 +3478,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -3511,6 +3580,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -3575,6 +3650,67 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.0.2.tgz",
"integrity": "sha512-m5AcPfTRUcjwmhBzOJGEl6Y7+Crqyju0+TgTQxoS4SO+BkWbhOrcfZNq6wSWdl2BBbJbsAoBUb8ZacOFT+/JlA==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0",
"turbo-stream": "2.4.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.0.2.tgz",
"integrity": "sha512-VJOQ+CDWFDGaWdrG12Nl+d7yHtLaurNgAQZVgaIy7/Xd+DojgmYLosFfZdGz1wpxmjJIAkAMVTKWcvkx1oggAw==",
"license": "MIT",
"dependencies": {
"react-router": "7.0.2"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@ -3701,6 +3837,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safe-regex-test": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
@ -3738,6 +3880,12 @@
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@ -3772,6 +3920,12 @@
"node": ">= 0.4"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3824,6 +3978,15 @@
"node": ">=0.10.0"
}
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/string.prototype.matchall": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
@ -3953,6 +4116,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/turbo-stream": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz",
"integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==",
"license": "ISC"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -4100,6 +4269,12 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/vite": {
"version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",

View file

@ -4,14 +4,17 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"animejs": "^3.2.2",
"jszip": "^3.10.1",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
"@eslint/js": "^9.13.0",

View file

@ -1,13 +1,18 @@
button {
height: 1.2cm;
text-decoration: none;
font-weight: bold;
border-radius: 0.6cm;
padding: 0cm 0.75cm 0cm 0.75cm;
margin-right: 0.25cm;
margin-bottom: 0.25cm;
font-family: "Source Sans 3";
font-size: 20pt;
font-weight: 900;
text-align: center;
border: none;
border-radius: 30px;
padding: 8px 52px 8px 52px;
transition-duration: 0.2s;
display: flex;
align-items: center;
}
button.gray{
background-color: #222222;
}

18
src/css/customInput.css Normal file
View file

@ -0,0 +1,18 @@
.input{
background-color: #ffffff;
color: #000;
text-decoration: none;
font-family: "Source Sans 3";
font-size: 20pt;
font-weight: 700;
text-align: center;
border: none;
border-radius: 30px;
/* padding: 8px 52px 8px 52px; */
height: 51px;
width: 249;
transition-duration: 0.2s;
display: flex;
align-items: center;
}

View file

@ -8,16 +8,28 @@
}
html * {
background-color: rgb(var(--black));
/* background-color: rgb(var(--black)); */
color: rgb(var(--white));
}
body {
margin: 0;
background-color: rgb(var(--black));
}
.title {
font-family: "Source Sans 3";
font-size: 36pt;
font-weight: 900;
text-align: center;
}
.title.medium{
font-size: 24pt;
}
.body {
font-family: "Source Sans 3";
font-size: 24pt;
font-weight: 400;
text-align: center;
}

View file

@ -0,0 +1,21 @@
.character {
width: 624px; /* Width of a single frame */
height: 814px; /* Height of a single frame */
background-color: wheat;
/* overflow: hidden; */
/* Ensure only one frame is visible */
}
.spriteAnimation {
width: 9984px;
animation: moveSprite 1s steps(16) infinite; /* 240 total frames */
}
@keyframes moveSprite {
form{
transform: translate3d(0,0,0)
}
to{
transform: translate3d(-100%,0,0);
}
}

View file

@ -1,10 +1,27 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './css/global.css'
import HomePage from './pages/homePage.jsx'
import * as React from 'react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import './css/global.css';
import HomePage from './pages/homePage.jsx';
import WarningPage from './pages/warningPage.jsx';
import NamePage from './pages/namePage.jsx';
import Animation from './pages/components/animation.jsx';
import IntroductionPage from './pages/introductionPage.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<HomePage />
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/warn" element={<WarningPage />} />
<Route path="/name" element={<NamePage />} />
<Route path="/sprite" element={<Animation />} />
<Route path="/introduction" element={<IntroductionPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</BrowserRouter>
</StrictMode>,
)
);

View file

@ -0,0 +1,195 @@
import React, { useEffect, useRef } from 'react';
import JSZip from 'jszip';
const Animation = ({ src, animationWidth }) => {
const canvasRef = useRef(null);
const imageElementsRef = useRef([]);
const animationRef = useRef(null);
const currentFrameRef = useRef(0);
const loadingRef = useRef(true);
const frameRate = 1000 / 24;
useEffect(() => {
const loadImages = async () => {
const zip = new JSZip();
try {
const response = await fetch(src);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.arrayBuffer();
const zipContent = await zip.loadAsync(data);
const imgPromises = [];
zipContent.forEach((relativePath, file) => {
if (file.name.endsWith('.webp')) {
imgPromises.push(
file.async('base64').then(base64 => {
const img = new Image();
img.src = `data:image/webp;base64,${base64}`;
return new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});
})
);
}
});
imageElementsRef.current = await Promise.all(imgPromises);
if (imageElementsRef.current.length === 0) {
console.error('No images found in the ZIP file.');
}
loadingRef.current = false; // Set loading to false after images are loaded
} catch (error) {
console.error('Error loading images:', error);
}
};
loadImages();
}, [src]);
useEffect(() => {
if (loadingRef.current || imageElementsRef.current.length === 0) return;
const canvas = canvasRef.current;
const gl = canvas.getContext('webgl');
if (!gl) {
console.error("WebGL is not supported.");
return;
}
// Set up WebGL context (basic)
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.viewport(0, 0, canvas.width, canvas.height);
// Enable blending
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// gl.blendFunc(gl.SRC_COLOR, gl.DST_COLOR);
// Shader program
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`;
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
`;
const createShader = (source, type) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('ERROR compiling shader', gl.getShaderInfoLog(shader));
}
return shader;
};
const vertexShader = createShader(vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error('ERROR linking program', gl.getProgramInfoLog(shaderProgram));
}
gl.useProgram(shaderProgram);
// Create buffer and set up attributes
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const vertices = new Float32Array([
-1.0, -1.0, // Bottom-left
1.0, -1.0, // Bottom-right
-1.0, 1.0, // Top-left
1.0, 1.0 // Top-right
]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(shaderProgram, "a_position");
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
// Create texture and load image
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const loadTexture = (image) => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
};
loadTexture(imageElementsRef.current[0]); // Initially load the first image
// Texture coordinates
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
const texCoords = new Float32Array([
0.0, 1.0, // Bottom-left (now corresponds to top-left of the image)
1.0, 1.0, // Bottom-right (now corresponds to top-right of the image)
0.0, 0.0, // Top-left (now corresponds to bottom-left of the image)
1.0, 0.0 // Top-right (now corresponds to bottom-right of the image)
]);
gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
const texCoordLocation = gl.getAttribLocation(shaderProgram, "a_texCoord");
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texCoordLocation);
const uTextureLocation = gl.getUniformLocation(shaderProgram, "u_texture");
let lastFrameTime = 0;
const animate = (timestamp) => {
if (lastFrameTime === 0) lastFrameTime = timestamp;
const elapsed = timestamp - lastFrameTime;
if (elapsed > frameRate) {
currentFrameRef.current = (currentFrameRef.current + 1) % images.length;
loadTexture(imageElementsRef.current[currentFrameRef.current]);
lastFrameTime = timestamp;
}
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationRef.current);
}, [imageElementsRef.current]);
if (loadingRef.current) {
return <div>Loading...</div>;
}
return (
<canvas ref={canvasRef} width={1920} height={1080} style={{ maxWidth: animationWidth, height: 'auto' }} />
);
};
export default Animation;

View file

@ -0,0 +1,403 @@
//version 1
// import React, { useEffect, useState } from 'react';
// import JSZip from 'jszip';
// const Animation = ({src="src/assets/mainCharacters/M_Porsche_cross_arm.zip" ,animationWidth="500px"}) => {
// const [images, setImages] = useState([]);
// const [currentFrame, setCurrentFrame] = useState(0);
// const [loading, setLoading] = useState(true);
// const frameRate = 1000/24;
// useEffect(() => {
// const loadImages = async () => {
// const zip = new JSZip();
// try {
// const response = await fetch(src);
// if (!response.ok) {
// throw new Error('Network response was not ok');
// }
// const data = await response.arrayBuffer();
// const zipContent = await zip.loadAsync(data);
// const imgPromises = [];
// zipContent.forEach((relativePath, file) => {
// if (file.name.endsWith('.webp')) {
// imgPromises.push(
// file.async('base64').then(base64 => {
// return `data:image/webp;base64,${base64}`;
// })
// );
// }
// });
// const imgUrls = await Promise.all(imgPromises);
// if (imgUrls.length === 0) {
// console.error('No images found in the ZIP file.');
// }
// setImages(imgUrls);
// } catch (error) {
// console.error('Error loading images:', error);
// } finally {
// setLoading(false);
// }
// };
// loadImages();
// }, []);
// useEffect(() => {
// if (images.length > 0) {
// const interval = setInterval(() => {
// setCurrentFrame((prevFrame) => (prevFrame + 1) % images.length);
// }, frameRate);
// return () => clearInterval(interval);
// }
// }, [images]);
// if (loading) {
// return <div>Loading...</div>;
// } else {
// return (
// <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
// {images.length > 0 ? (
// <img
// src={images[currentFrame]}
// alt={`Animation frame ${currentFrame + 1}`}
// style={{ maxWidth: animationWidth, height: 'auto' }}
// />
// ) : (
// <div>No images to display.</div>
// )}
// </div>
// );
// }
// };
// export default Animation;
//version 2
// import React, { useEffect, useState, useRef } from 'react';
// import JSZip from 'jszip';
// const Animation = ({ src = "src/assets/mainCharacters/M_Porsche_cross_arm.zip", animationWidth = "500px" }) => {
// const [images, setImages] = useState([]);
// const [loading, setLoading] = useState(true);
// const frameRate = 1000 / 24; // Original frame rate (24 FPS)
// const canvasRef = useRef(null);
// const animationRef = useRef(null);
// const currentFrameRef = useRef(0);
// const imageElementsRef = useRef([]);
// useEffect(() => {
// const loadImages = async () => {
// const zip = new JSZip();
// try {
// const response = await fetch(src);
// if (!response.ok) {
// throw new Error('Network response was not ok');
// }
// const data = await response.arrayBuffer();
// const zipContent = await zip.loadAsync(data);
// const imgPromises = [];
// zipContent.forEach((relativePath, file) => {
// if (file.name.endsWith('.webp')) {
// imgPromises.push(
// file.async('base64').then(base64 => {
// const img = new Image();
// img.src = `data:image/webp;base64,${base64}`;
// return img; // Return the Image object
// })
// );
// }
// });
// const imgElements = await Promise.all(imgPromises);
// if (imgElements.length === 0) {
// console.error('No images found in the ZIP file.');
// }
// imageElementsRef.current = imgElements; // Store preloaded images
// setImages(imgElements); // Set images state
// } catch (error) {
// console.error('Error loading images:', error);
// } finally {
// setLoading(false);
// }
// };
// loadImages();
// }, [src]);
// useEffect(() => {
// if (images.length > 0) {
// const canvas = canvasRef.current;
// const ctx = canvas.getContext('2d');
// let lastFrameTime = 0;
// const animate = (timestamp) => {
// if (lastFrameTime === 0) lastFrameTime = timestamp;
// const elapsed = timestamp - lastFrameTime;
// if (elapsed > frameRate) {
// currentFrameRef.current = (currentFrameRef.current + 1) % images.length;
// lastFrameTime = timestamp;
// }
// // Clear the canvas
// ctx.clearRect(0, 0, canvas.width, canvas.height);
// // Draw the current frame
// ctx.drawImage(imageElementsRef.current[currentFrameRef.current], 0, 0, canvas.width, canvas.height);
// animationRef.current = requestAnimationFrame(animate);
// };
// animationRef.current = requestAnimationFrame(animate);
// return () => {
// if (animationRef.current) {
// cancelAnimationFrame(animationRef.current);
// }
// };
// }
// }, [images]);
// if (loading) {
// return <div>Loading...</div>;
// } else {
// return (
// <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
// <canvas
// ref={canvasRef}
// width={'500px'}
// height={'700'} // Maintain aspect ratio (example: 16:9)
// style={{ maxWidth: animationWidth, height: 'auto' }}
// />
// </div>
// );
// }
// };
// export default Animation;
import React, { useEffect, useState, useRef } from 'react';
import JSZip from 'jszip';
const Animation = ({ src = "https://fsob-assets.techtransthai.org/M_Porsche_cross_arm.zip", animationWidth = "500px" }) => {
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(true);
const frameRate = 1000 / 24; // Original frame rate (24 FPS)
const canvasRef = useRef(null);
const animationRef = useRef(null);
const currentFrameRef = useRef(0);
const imageElementsRef = useRef([]);
// Load images from the ZIP file
useEffect(() => {
const loadImages = async () => {
const zip = new JSZip();
try {
const response = await fetch(src);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.arrayBuffer();
const zipContent = await zip.loadAsync(data);
const imgPromises = [];
zipContent.forEach((relativePath, file) => {
if (file.name.endsWith('.webp')) {
imgPromises.push(
file.async('base64').then(base64 => {
const img = new Image();
img.src = `data:image/webp;base64,${base64}`;
return new Promise((resolve, reject) => {
img.onload = () => resolve(img);
img.onerror = reject;
});
})
);
}
});
const imgElements = await Promise.all(imgPromises);
if (imgElements.length === 0) {
console.error('No images found in the ZIP file.');
}
imageElementsRef.current = imgElements; // Store preloaded images
setImages(imgElements); // Set images state
} catch (error) {
console.error('Error loading images:', error);
} finally {
setLoading(false);
}
};
loadImages();
}, [src]);
// WebGL setup and animation loop
useEffect(() => {
if (images.length > 0) {
const canvas = canvasRef.current;
const gl = canvas.getContext('webgl');
if (!gl) {
console.error("WebGL is not supported.");
return;
}
// Set up WebGL context (basic)
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.viewport(0, 0, canvas.width, canvas.height);
// Enable blending
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// gl.blendFunc(gl.SRC_COLOR, gl.DST_COLOR);
// Shader program
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}
`;
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_texture, v_texCoord);
}
`;
const createShader = (source, type) => {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error('ERROR compiling shader', gl.getShaderInfoLog(shader));
}
return shader;
};
const vertexShader = createShader(vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = createShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
const shaderProgram = gl.createProgram();
gl.attachShader(shaderProgram, vertexShader);
gl.attachShader(shaderProgram, fragmentShader);
gl.linkProgram(shaderProgram);
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
console.error('ERROR linking program', gl.getProgramInfoLog(shaderProgram));
}
gl.useProgram(shaderProgram);
// Create buffer and set up attributes
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const vertices = new Float32Array([
-1.0, -1.0, // Bottom-left
1.0, -1.0, // Bottom-right
-1.0, 1.0, // Top-left
1.0, 1.0 // Top-right
]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(shaderProgram, "a_position");
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLocation);
// Create texture and load image
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
const loadTexture = (image) => {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.generateMipmap(gl.TEXTURE_2D);
};
loadTexture(imageElementsRef.current[0]); // Initially load the first image
// Texture coordinates
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
const texCoords = new Float32Array([
0.0, 1.0, // Bottom-left (now corresponds to top-left of the image)
1.0, 1.0, // Bottom-right (now corresponds to top-right of the image)
0.0, 0.0, // Top-left (now corresponds to bottom-left of the image)
1.0, 0.0 // Top-right (now corresponds to bottom-right of the image)
]);
gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
const texCoordLocation = gl.getAttribLocation(shaderProgram, "a_texCoord");
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texCoordLocation);
const uTextureLocation = gl.getUniformLocation(shaderProgram, "u_texture");
let lastFrameTime = 0;
const animate = (timestamp) => {
if (lastFrameTime === 0) lastFrameTime = timestamp;
const elapsed = timestamp - lastFrameTime;
if (elapsed > frameRate) {
currentFrameRef.current = (currentFrameRef.current + 1) % images.length;
loadTexture(imageElementsRef.current[currentFrameRef.current]);
lastFrameTime = timestamp;
}
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
animationRef.current = requestAnimationFrame(animate);
};
animationRef.current = requestAnimationFrame(animate);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}
}, [images]);
if (loading) {
return <div>Loading...</div>;
} else {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<canvas
ref={canvasRef}
width={'1920'}
height={'1080'} // Maintain aspect ratio (example: 16:9)
style={{ maxWidth: animationWidth, height: 'auto' }}
/>
</div>
);
}
};
export default Animation;

View file

@ -1,10 +1,12 @@
import '../../css/customButton.css'
import { useNavigate } from 'react-router-dom';
function BlackButton({ text }) {
function BlackButton({ text , to="/"}) {
const navigate = useNavigate();
return(
<button>{text}</button>
)
}
return(
<button className='gray' onClick={()=> navigate(to, { replace: true })}>{text}</button>
)
}
export default BlackButton

View file

@ -0,0 +1,46 @@
import React, { useEffect, useRef, useState } from 'react';
import anime from 'animejs';
const startNumber = 1000;
const endNumber = 1239;
const imageCount = endNumber - startNumber + 1;
const images = [];
for (let i = startNumber; i <= endNumber; i++) {
const formattedNumber = String(i).padStart(5, '0');
images.push(import(`../../assets/characters/F_porche_akimbo_AME/Porsche${formattedNumber}.png`));
}
const CharacterAnimation = () => {
const totalFrames = imageCount;
const frameDuration = 20;
const [currentFrame, setCurrentFrame] = useState(0);
const [loadedImages, setLoadedImages] = useState([]);
const characterRef = useRef(null);
useEffect(() => {
Promise.all(images).then((resolvedImages) => {
setLoadedImages(resolvedImages.map(image => image.default));
});
}, []);
useEffect(() => {
const interval = setInterval(() => {
setCurrentFrame((prevFrame) => (prevFrame + 1) % totalFrames);
}, frameDuration);
return () => clearInterval(interval);
}, []);
return (
<img
ref={characterRef}
src={loadedImages[currentFrame]}
alt="Character Animation"
style={{ width: '200px', height: 'auto' }}
/>
);
};
export default CharacterAnimation;

View file

@ -0,0 +1,81 @@
import React, { useEffect, useState } from 'react';
import JSZip from 'jszip';
const TestAnimation = () => {
const [images, setImages] = useState([]);
const [currentFrame, setCurrentFrame] = useState(0);
const [loading, setLoading] = useState(true);
const frameRate = 60;
useEffect(() => {
const loadImages = async () => {
const zip = new JSZip();
try {
const response = await fetch('src/assets/mainCharacters/M_Porsche_cross_arm.zip');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.arrayBuffer();
const zipContent = await zip.loadAsync(data);
const imgPromises = [];
zipContent.forEach((relativePath, file) => {
if (file.name.endsWith('.webp')) {
imgPromises.push(
file.async('base64').then(base64 => {
return `data:image/webp;base64,${base64}`;
})
);
}
});
const imgUrls = await Promise.all(imgPromises);
// console.log('Loaded images:', imgUrls);
if (imgUrls.length === 0) {
console.error('No images found in the ZIP file.');
}
setImages(imgUrls);
} catch (error) {
console.error('Error loading images:', error);
} finally {
setLoading(false);
}
};
loadImages();
}, []);
useEffect(() => {
if (images.length > 0) {
const interval = setInterval(() => {
setCurrentFrame((prevFrame) => (prevFrame + 1) % images.length);
}, frameRate);
return () => clearInterval(interval);
}
}, [images]);
if (loading) {
return <div>Loading...</div>;
} else {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
{images.length > 0 ? (
<img
src={images[currentFrame]}
alt={`Animation frame ${currentFrame + 1}`}
style={{ maxWidth: '500px', height: 'auto' }}
/>
) : (
<div>No images to display.</div>
)}
</div>
);
}
};
export default TestAnimation;

View file

@ -13,8 +13,11 @@ function HomePage() {
height: '100vh',
flexDirection: 'column',
}}>
<a className='title'>Fifty Shades <br /> of Bully</a>
<BlackButton text="Start"/>
<label className='title'>Fifty Shades <br /> of Bully</label>
<div style={{ height: '8vh' }}/>
<BlackButton text="Start" to='/warn'/>
</div>
)
}

View file

@ -0,0 +1,58 @@
import { useState } from 'react'
import '../css/global.css'
import BlackButton from './components/customButton'
import Animation from './components/animation.jsx';
function IntroductionPage() {
return(
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
maxWidth:"100vw",
flexDirection: 'column',
}}>
<div style={{position:"absolute"}}>
<Animation src='src/assets/introduction/6_Classroom.zip' animationWidth='100vw'/>
</div>
<div style={{position:"absolute"}}>
<Animation src='src/assets/introduction/5_M_Pie.zip' animationWidth='100vw'/>
</div>
<div style={{position:"absolute"}}>
<Animation src='src/assets/introduction/4_NPC+hallway.zip' animationWidth='100vw'/>
</div>
<div style={{position:"absolute"}}>
<Animation src='src/assets/introduction/3_M_Patt.zip' animationWidth='100vw'/>
</div>
<div style={{position:"absolute"}}>
<Animation src='src/assets/introduction/2_M_Porsche.zip' animationWidth='100vw'/>
</div>
<div style={{position:"absolute"}}>
<Animation src='src/assets/introduction/1_NPC+pillar.zip' animationWidth='100vw'/>
</div>
<div style={{
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
background: "rgba(var(--black), 0.5)",
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1 // Ensure this is above other elements
}}>
<label style={{color:"rgb(var(--white))", fontWeight:"bold"}}>
You are a new student who just <br />
transfered to this school not long ago. <br />
You don't know anyone yet, <br />
but eventually you meet...
</label>
</div>
</div>
)
}
export default IntroductionPage

38
src/pages/namePage.jsx Normal file
View file

@ -0,0 +1,38 @@
import '../css/global.css'
import '../css/customInput.css'
import BlackButton from './components/customButton'
import { useState } from 'react';
function NamePage() {
const [name, setName] = useState('');
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
flexDirection: 'column',
}}>
<label className='title'>What should we call you?</label>
<div style={{
height: '8vh'
}}/>
<input className='input' value={name} onChange={e => setName(e.target.value)}/>
<div style={{
height: '8vh'
}}/>
{
name==''?
<></>
:
<BlackButton text="Continue" to='/introduction'/>
}
</div>
)
}
export default NamePage

36
src/pages/warningPage.jsx Normal file
View file

@ -0,0 +1,36 @@
import { useState } from 'react'
import '../css/global.css'
import { useNavigate } from 'react-router-dom';
function WarningPage() {
const navigate = useNavigate();
return(
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
flexDirection: 'column',
}}
onClick={()=>{navigate('/name', { replace: true })}}
>
<label className='title'>Trigger warning</label>
<div style={{
height: '8vh'
}}/>
<label className='body'>
Lorem ipsum dolor sit amet, <br/>
consectetur adipiscing elit, <br/>
sed do eiusmod tempor <br/>
incididunt ut labore et dolore <br/>
magnam aliquam quaerat <br/>
voluptatem.
</label>
</div>
)
}
export default WarningPage

View file

@ -4,4 +4,5 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
assetsInclude: ['**/*.zip'],
})