diff --git a/.gitignore b/.gitignore index ceaea36..19c165c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,79 +1,105 @@ -# ---> Node +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + # Logs + logs -*.log -npm-debug.log* +_.log +npm-debug.log_ yarn-debug.log* yarn-error.log* lerna-debug.log* .pnpm-debug.log* +# Caches + +.cache + # Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Runtime data + pids -*.pid -*.seed +_.pid +_.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover + lib-cov # Coverage directory used by tools like istanbul + coverage *.lcov # nyc test coverage + .nyc_output # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + .grunt # Bower dependency directory (https://bower.io/) + bower_components # node-waf configuration + .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) + build/Release # Dependency directories + node_modules/ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) + web_modules/ # TypeScript cache + *.tsbuildinfo # Optional npm cache directory + .npm # Optional eslint cache + .eslintcache # Optional stylelint cache + .stylelintcache # Microbundle cache + .rpt2_cache/ .rts2_cache_cjs/ .rts2_cache_es/ .rts2_cache_umd/ # Optional REPL history + .node_repl_history # Output of 'npm pack' + *.tgz # Yarn Integrity file + .yarn-integrity # dotenv environment variable files + .env .env.development.local .env.test.local @@ -81,52 +107,72 @@ web_modules/ .env.local # parcel-bundler cache (https://parceljs.org/) -.cache + .parcel-cache # Next.js build output + .next out # Nuxt.js build / generate output + .nuxt dist # Gatsby files -.cache/ + # Comment in the public line in if your project uses Gatsby and not Next.js + # https://nextjs.org/blog/next-9-1#public-directory-support + # public # vuepress build output + .vuepress/dist # vuepress v2.x temp and cache directory + .temp -.cache # Docusaurus cache and generated files + .docusaurus # Serverless directories + .serverless/ # FuseBox cache + .fusebox/ # DynamoDB Local files + .dynamodb/ # TernJS port file + .tern-port # Stores VSCode versions used for testing VSCode extensions + .vscode-test # yarn v2 + .yarn/cache .yarn/unplugged .yarn/build-state.yml .yarn/install-state.gz .pnp.* +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +dist +src/web/dist.js \ No newline at end of file diff --git a/README.md b/README.md index d9bfd3b..fb2e46a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# website - +# sophie's website +this website is very simple it is located in src/web. +there is a very sophisticated build script in src/build.ts ! +it even supports HMR! \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..5777755 Binary files /dev/null and b/bun.lockb differ diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..08bb702 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,12 @@ +import P5 from "p5" +import * as p5Sound from 'p5/lib/addons/p5.sound' +import * as p5Global from 'p5/global' + +export = P5; +export as namespace p5; + +declare global { + interface Window { + p5: typeof P5, + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..edd2991 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "sophie", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^20.11.30", + "@types/p5": "^1.7.6", + "@types/serve-handler": "^6.1.4", + "esbuild": "0.20.2", + "micro": "^10.0.1", + "serve-handler": "^6.1.5" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "scripts": { + "dev": "bun src/build.ts --dev", + "build": "bun src/build.ts --build" + }, + "dependencies": { + "ws": "^8.16.0" + } +} diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 0000000..209dca7 --- /dev/null +++ b/src/build.ts @@ -0,0 +1,102 @@ +import * as esbuild from "esbuild" +import {watch, cpSync, rmdirSync, createReadStream, readFileSync, ReadStream} from "node:fs" +import { serve } from 'micro'; +import {Server} from "node:http" +import handler from "serve-handler" +import { Readable } from "node:stream"; +import { WebSocketServer } from 'ws'; + +const argv = process.argv.slice(2) + +function buildTs() { + console.log("[dev] Building file..") + esbuild.build({ + entryPoints: ["./src/web/ts/index.ts"], + outfile: "./src/web/dist.js" + }) +} +buildTs(); + +if(argv[0] == "--build") { + try{rmdirSync("./dist")}catch{} + cpSync("./src/web/", "./dist/", {recursive:true}) + rmdirSync("./dist/ts",{recursive:true}) + console.log("[dev] View the dist folder") +} + +if(argv[0] == "--dev") { + const scriptName = Math.random().toFixed(10).replace("0.","") + + const wss = new WebSocketServer({ port: 8081 }); + let allConnections: any[] = []; + + wss.on('connection', function connection(ws) { + //@ts-ignore + ws.id = Math.random(); + //@ts-ignore + + allConnections.push(ws); + + ws.on('error', console.error); + ws.on("close", () => { + //@ts-ignore + allConnections = allConnections.filter(z => z.id != ws.id); + }) + }); + const server = new Server( + serve(async (req, res) => { + if(req.url == `/${scriptName}.js`) { + const body = `let t;function rr() {console.log("connecting to dev server") + let a = new WebSocket("ws://localhost:8081"); + a.addEventListener("message", g => { + if(g.data == "refresh"){ + location.reload() + } + }); + a.addEventListener("open", () => { + console.log("connected") + }) + a.addEventListener("close", () => { + console.log("socket closed, restarting in 1s") + clearInterval(t) + t = setTimeout(()=>{ + rr() + },1000) + })};rr();`; + res + .writeHead(200, { + 'Content-Length': Buffer.byteLength(body), + 'Content-Type': 'text/plain', + }) + .end(body); + } + if(req.url == "/" || req.url?.endsWith(".html")) { + await handler(req, res, { + directoryListing: false, + public: "src/web/" + }, { + createReadStream(path, options) { + let sx = readFileSync(path).toString("utf8"); + sx = sx.replaceAll("", ``) + return Readable.from([sx]) as ReadStream + }, + }); + } else { + await handler(req, res, { + directoryListing: false, + public: "src/web/" + }); + } + }) + ) + server.listen(8080) + console.log('[http] Listening HTTP on 8080.') + const watcher = watch("./src/web", { + recursive: true, + }, (e,f) => { + if(f == "dist.js") return; + console.log("[dev] Noticed update in " + f + ", of type " + e +".") + allConnections.forEach(z => z.send("refresh")); + buildTs(); + }) +} diff --git a/src/web/Screenshot_2023-02-04_16-45-32.png b/src/web/Screenshot_2023-02-04_16-45-32.png new file mode 100755 index 0000000..96dfd04 Binary files /dev/null and b/src/web/Screenshot_2023-02-04_16-45-32.png differ diff --git a/src/web/index.html b/src/web/index.html new file mode 100644 index 0000000..36a51b4 --- /dev/null +++ b/src/web/index.html @@ -0,0 +1,109 @@ + + + + + + + + + + + sophie's personal site + + + + + + + + + + + + + + + + + + + + + + + + + +
+ | +
+ + I'm Latvian, 17. My name's Sophie. I love listening to music, I have <loading...> song plays. +
+ I am a JS/TS developer, mostly specializing in backend work. +
+ I play minecraft, and upgun with friends. I have developed many bukkit plugins for Paper/Folia/Spigot before, and am pretty well versed in them. +
+ Contact me at matrix, discord, github (view projects here), sadgit, or mastodon +
+ +
+ + + Click spacebar to change the themes. + + + + \ No newline at end of file diff --git a/src/web/sticker.webp b/src/web/sticker.webp new file mode 100644 index 0000000..52f3494 Binary files /dev/null and b/src/web/sticker.webp differ diff --git a/src/web/ts/index.ts b/src/web/ts/index.ts new file mode 100644 index 0000000..fb5231c --- /dev/null +++ b/src/web/ts/index.ts @@ -0,0 +1,370 @@ +/* + The original version of this Processing script was written by + Jason Labbe (available here: https://openprocessing.org/sketch/377231/) + + This is a highly modified version by Sophie, firstly re-written using JS, and then + improved using newer techniques (no more pGraphics, using font toPoints now as an example). + Jason Labbe's version is licensed under CC BY-SA 3.0 DEED, and so is this version. + + https://creativecommons.org/licenses/by-sa/3.0/deed.en +*/ + +// Global variables +let particles: Particle[] = []; +let font: p5.Font; +let bgColor: p5.Color; + +let darkMode = false; + +let darkTheme: p5.Color; +let lightTheme: p5.Color; + +interface Bounds { + x: number; + y: number; + w: number; + h: number; +} + +function generateRandomPos(x: number, y: number, mag: number) { + let randomDir = createVector(random(0, width), random(0, height)); + + let pos = createVector(x, y); + pos.sub(randomDir); + pos.normalize(); + pos.mult(mag); + pos.add(x, y); + + return pos; +} + +function getTextSize(text: string, font: p5.Font) { + let fontSize = 1200; // Wait, doesn't this impact performance heavily? + // Nope, a single iteration (once warmed up) only takes around + // 0.04799999999254942ms. This means, that for a 8K screen it'd take + // 11ms, and for normal 1080P screen it takes 20-40ms (891 iterations). + // This can be optimized further, but it's a single run of this function + // every time the word is rerendered. I don't think it matters nearly as much.. + // Pouring too much time into this single number.. what the hell + + let bounds: Bounds; + + //let iters = 0; + //let time = 0; + do { + //const loopstart = performance.now(); + fontSize--; + bounds = font.textBounds(text, 0, 0, fontSize) as Bounds; + // const loopend = performance.now(); + // time += loopend - loopstart; + // iters += 1; + } while (bounds.w > windowWidth - 100); + //console.log("Took " + time + "ms for " + iters + " iterations"); + return fontSize; +} + +class Particle { + pos: p5.Vector; + vel: p5.Vector; + acc: p5.Vector; + + target: p5.Vector; + + closeEnoughTarget: number; + maxSpeed: number; + maxForce: number; + particleSize: number; + isKilled: boolean; + startColor: p5.Color; + targetColor: p5.Color; + colorWeight: number; + colorBlendRate: number; + + constructor() { + this.pos = createVector(0, 0); + this.vel = createVector(0, 0); + this.acc = createVector(0, 0); + this.target = createVector(0, 0); + + this.closeEnoughTarget = 50; + this.maxSpeed = 4.0; + this.maxForce = 0.1; + this.particleSize = 5; + this.isKilled = false; + + this.startColor = color(0); + this.targetColor = color(0); + this.colorWeight = 0; + this.colorBlendRate = 0.025; + } + + move() { + // Check if particle is close enough to its target to slow down + let proximityMult = 1.0; + let distance = dist(this.pos.x, this.pos.y, this.target.x, this.target.y); + if (distance < this.closeEnoughTarget) { + proximityMult = distance / this.closeEnoughTarget; + } + + //.push force towards target + let towardsTarget = createVector(this.target.x, this.target.y); + towardsTarget.sub(this.pos); + towardsTarget.normalize(); + towardsTarget.mult(this.maxSpeed * proximityMult); + + let steer = createVector(towardsTarget.x, towardsTarget.y); + steer.sub(this.vel); + steer.normalize(); + steer.mult(this.maxForce); + this.acc.add(steer); + + // Move particle + this.vel.add(this.acc); + this.pos.add(this.vel); + this.acc.mult(0); + } + + draw() { + // Draw particle + let currentColor = lerpColor( + this.startColor, + this.targetColor, + this.colorWeight + ); + + noStroke(); + fill(currentColor); + ellipse(this.pos.x, this.pos.y, this.particleSize, this.particleSize); + + // Blend towards its target color + if (this.colorWeight < 1.0) { + this.colorWeight = min(this.colorWeight + this.colorBlendRate, 1.0); + } + } + + kill() { + if (!this.isKilled) { + // Set its target outside the scene + let randomPos = generateRandomPos( + width / 2, + height / 2, + (width + height) / 2 + ); + this.target.x = randomPos.x; + this.target.y = randomPos.y; + + // Begin blending its color to black + this.startColor = lerpColor( + this.startColor, + this.targetColor, + this.colorWeight + ); + this.targetColor = color(0); + this.colorWeight = 0; + + this.isKilled = true; + } + } +} + +function preload() { + font = loadFont("https://rsms.me/inter/font-files/InterVariable.ttf"); + darkTheme = color(34, 40, 49, 255); + lightTheme = color(238, 238, 238, 255); +} + +function nextWord(word: string) { + const fontSize = getTextSize(word, font); + const bounds = font.textBounds(word, 0, 0, fontSize) as Bounds; + let sampleFactor = 0.2; + + if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ + sampleFactor = 0.1; + } + + let points = font.textToPoints( + word, + (windowWidth - 25 - bounds.w) / 2, + (windowHeight + bounds.h / 2) / 2, + fontSize, + { + sampleFactor, + simplifyThreshold: 0, + } + ); + + let newColor = color( + random(0.0, 255.0), + random(0.0, 255.0), + random(0.0, 255.0) + ); + + let particleCount = particles.length; + let particleIndex = 0; + + for (let i = 0; i < points.length; i++) { + let randomIndex = Math.floor(random(0, points.length)); + const point = points[randomIndex]; + + let newParticle; + + if (particleIndex < particleCount) { + newParticle = particles[particleIndex]; + newParticle.isKilled = false; + particleIndex += 1; + } else { + newParticle = new Particle(); + + let randomPos = generateRandomPos( + width / 2, + height / 2, + (width + height) / 2 + ); + newParticle.pos.x = randomPos.x; + newParticle.pos.y = randomPos.y; + + newParticle.maxSpeed = random(2.0, 5.0); + newParticle.maxForce = newParticle.maxSpeed * 0.025; + newParticle.particleSize = random(3, 6); + newParticle.colorBlendRate = random(0.0025, 0.03); + + particles.push(newParticle); + } + + newParticle.startColor = lerpColor( + newParticle.startColor, + newParticle.targetColor, + newParticle.colorWeight + ); + newParticle.targetColor = newColor; + newParticle.colorWeight = 0; + + newParticle.target.x = point.x; + newParticle.target.y = point.y; + } + + if (particleIndex < particleCount) { + for (let i = particleIndex; i < particleCount; i++) { + let particle = particles[i]; + particle.kill(); + } + } +} + +function windowResized() { + resizeCanvas(windowWidth, windowHeight); +} + +function setup() { + // selection + let pages = ["home", "donations"]; + let selected = "home"; + pages.forEach(z => { + const buttonElement = document.getElementById(z+"-button")!; + const element = document.getElementById(z)!; + buttonElement.addEventListener("click", _ => { + if(z !== selected) { + const selectedElement = document.getElementById(selected)! + const selectedButton = document.getElementById(selected+"-button")! + selectedElement.style.display = "none"; + element.style.display = "block"; + selectedButton.className = "" + buttonElement.className = "selected"; + selected = z; + + + } + }) + }); + + // lastfm + (async () => { + const lastfm = await fetch( + "https://ws.audioscrobbler.com/2.0/?method=user.getinfo&user=yourfriendoss&api_key=8d983789c771afaeb7412ac358d4bad0&format=json" + ); + const json = await lastfm.json(); + document.getElementById("songplays")!.innerText = json.user.playcount; + })(); + + const theme = localStorage.getItem("theme"); + if (theme == "dark") { + bgColor = darkTheme; + darkMode = true; + } else { + bgColor = lightTheme; + darkMode = false; + } + document.body.setAttribute("data-theme", darkMode ? "dark" : "light"); + const canvas = createCanvas(windowWidth, windowHeight); + canvas.position(0, 0); + canvas.style("zIndex", "-1"); + background(bgColor); + const p = "sad.ovh" + nextWord(p); + + let ticks = 0; + + setInterval(() => { + const a = particles.filter(z => { + const x = Math.abs(Math.floor(z.vel.x)) + const y = Math.abs(Math.floor(z.vel.y)) + if (x == 1 || y == 1 || x == 0 || y == 0) { + return false; + } else { + return true; + } + }).length; + + if (a == 0) ticks++; + if (a != 0) ticks = 0; + + if (ticks == 60 * 8) { + ticks = 0; + nextWord(p) + } + }, 1) +} +function keyPressed(a: KeyboardEvent) { + if (a.code == "Space") { + if (darkMode) { + bgColor = lightTheme; + } else { + bgColor = darkTheme; + } + darkMode = !darkMode; + document.body.setAttribute("data-theme", darkMode ? "dark" : "light"); + localStorage.setItem("theme", darkMode ? "dark" : "light"); + } +} +function draw() { + fill(bgColor); + noStroke(); + rect(0, 0, width * 2, height * 2); + + for (let x = particles.length - 1; x > -1; x--) { + let particle = particles[x]; + particle.move(); + particle.draw(); + + if (particle.isKilled) { + if ( + particle.pos.x < 0 || + particle.pos.x > width || + particle.pos.y < 0 || + particle.pos.y > height + ) { + particles.splice(x, 1); + } + } + } +} + +function mouseDragged() { + if (mouseButton == LEFT) { + for (const particle of particles) { + if (dist(particle.pos.x, particle.pos.y, mouseX, mouseY) < 50) { + particle.kill(); + } + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}