diff --git a/src/Artist.ts b/src/Artist.ts deleted file mode 100644 index 7077dbdb..00000000 --- a/src/Artist.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { drawLine } from "$lib/canvasUtils"; - - -export class Artist{ - - ctx: CanvasRenderingContext2D; - rootX: number; - rootY: number; - startX: number; - startY: number; - endX: number; - endY: number; - - constructor(context: CanvasRenderingContext2D, x: number, y: number) { - this.startX = x; - this.startY = y; - this.endX = x; - this.endY = y; - this.rootX = x; - this.rootY = y; - this.ctx = context; - } - - reset(xOffset: number, yOffset: number) { - this.startX = this.rootX + xOffset; - this.startY = this.rootY + yOffset; - } - - drawNextLine(xOffset: number, yOffset: number) { - this.endX = this.startX + xOffset; - this.endY = this.startY + yOffset; - drawLine(this.ctx, this.startX, this.startY, this.endX, this.endY); - this.startX = this.endX; - this.startY = this.endY; - } - - drawShape(points: Points, reflect: boolean, - xOffset: number, yOffset: number, - lineWidth: number, stroke?: string, fill?: string | CanvasGradient) { - this.ctx.beginPath(); - this.ctx.moveTo(this.startX, this.startY); - points.forEach(p => this.drawNextLine(p[0], p[1])); - if (reflect) { - this.reset(xOffset, yOffset); - points.forEach(p => this.drawReflection(p[0], p[1])); - } - this.ctx.closePath(); - - if (stroke) { - this.ctx.lineCap = 'round'; - this.ctx.lineWidth = lineWidth; - this.ctx.strokeStyle = stroke; - this.ctx.stroke(); - } - if (fill) { - this.ctx.fillStyle = fill; - this.ctx.fill(); - } - } - - drawReflection(xOffset: number, yOffset: number) { - this.drawNextLine(xOffset * -1, yOffset); - } -} diff --git a/src/app.d.ts b/src/app.d.ts index a75fb5ee..da2f6f14 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -32,7 +32,8 @@ declare global { description?: string } - type Points = [number, number][] + type Point = [number, number] + type Points = Point[] type JSONValue = { [x: string]: string } diff --git a/src/lib/api/githubApi.ts b/src/lib/api/githubApi.ts new file mode 100644 index 00000000..69bc44d9 --- /dev/null +++ b/src/lib/api/githubApi.ts @@ -0,0 +1,83 @@ +import { error } from "@sveltejs/kit"; +import { EXTERNAL_CODE_LANGS, getLangShortName } from "../data/codeLangData"; + +export async function ghGet(url: string, token: string) { + return await fetch(url, { + method: "GET", + mode: "cors", + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + "X-GitHub-Api-Version": "2022-11-28", + }, + }); +} + +async function extractLanguageData(data: JSONArray, token: string) { + try { + const urls: string[] = data.flatMap((repo) => repo.languages_url); + const collated: Record[] = []; + await Promise.all( + urls.map(async (url: string) => { + const response = await ghGet(url, token); + const data = (await response.json()) as Record; + if (data) { + collated.push(data); + } + }), + ); + return parseLanguageObject(collated); + } catch { + error(500, "Error parsing coding language data from github"); + } +} + +export async function getGhLanguageData(url: string, token: string) { + try { + const response = await ghGet(url, token); + const data = (await response.json()) as JSONArray; + const languages = await extractLanguageData(data, token); + return languages; + } catch { + error(500, "Error retrieving data from github"); + } +} + +export function parseLanguageObject( + data: Record[], +): Record { + let total = 0; + const parsed: Record = {}; + for (const languages of data) { + for (const language of Object.keys(languages)) { + const lang = getLangShortName(language); + if (parsed[lang]) { + // use proportional lines of code intead + // parsed[lang] += parsed[key]; + parsed[lang] += 1; + } else { + // use proportional lines of code intead + // parsed[lang] = parsed[key]; + parsed[lang] = 1; + } + // total += parsed[key]; + total += 1; + } + } + + for (const language of Object.keys(EXTERNAL_CODE_LANGS)) { + const lang = getLangShortName(language); + if (parsed[lang]) { + parsed[lang] += EXTERNAL_CODE_LANGS[language]; + } else { + parsed[lang] = EXTERNAL_CODE_LANGS[language]; + } + total += EXTERNAL_CODE_LANGS[language]; + } + + for (const language of Object.keys(parsed)) { + parsed[language] = parsed[language] / total; + } + return parsed; +} diff --git a/src/lib/stravaApi.ts b/src/lib/api/stravaApi.ts similarity index 100% rename from src/lib/stravaApi.ts rename to src/lib/api/stravaApi.ts diff --git a/src/lib/canvas/Animator.ts b/src/lib/canvas/Animator.ts new file mode 100644 index 00000000..7fe5fe24 --- /dev/null +++ b/src/lib/canvas/Animator.ts @@ -0,0 +1,9 @@ +export abstract class Animator { + ctx: CanvasRenderingContext2D; + + constructor(context: CanvasRenderingContext2D) { + this.ctx = context; + } + + abstract animate(): void; +} diff --git a/src/lib/canvas/Artist.ts b/src/lib/canvas/Artist.ts new file mode 100644 index 00000000..cb6b9fdc --- /dev/null +++ b/src/lib/canvas/Artist.ts @@ -0,0 +1,68 @@ +import { drawLine } from "$lib/canvas/canvasUtils"; + +export class Artist { + ctx: CanvasRenderingContext2D; + rootX: number; + rootY: number; + startX: number; + startY: number; + endX: number; + endY: number; + + constructor(context: CanvasRenderingContext2D, x: number, y: number) { + this.startX = x; + this.startY = y; + this.endX = x; + this.endY = y; + this.rootX = x; + this.rootY = y; + this.ctx = context; + } + + reset(xOffset: number, yOffset: number) { + this.startX = this.rootX + xOffset; + this.startY = this.rootY + yOffset; + } + + drawNextLine(xOffset: number, yOffset: number) { + this.endX = this.startX + xOffset; + this.endY = this.startY + yOffset; + drawLine(this.ctx, this.startX, this.startY, this.endX, this.endY); + this.startX = this.endX; + this.startY = this.endY; + } + + drawShape( + points: Points, + reflect: boolean, + xOffset: number, + yOffset: number, + lineWidth: number, + stroke?: string, + fill?: string | CanvasGradient, + ) { + this.ctx.beginPath(); + this.ctx.moveTo(this.startX, this.startY); + points.forEach((p) => this.drawNextLine(p[0], p[1])); + if (reflect) { + this.reset(xOffset, yOffset); + points.forEach((p) => this.drawReflection(p[0], p[1])); + } + this.ctx.closePath(); + + if (stroke) { + this.ctx.lineCap = "round"; + this.ctx.lineWidth = lineWidth; + this.ctx.strokeStyle = stroke; + this.ctx.stroke(); + } + if (fill) { + this.ctx.fillStyle = fill; + this.ctx.fill(); + } + } + + drawReflection(xOffset: number, yOffset: number) { + this.drawNextLine(xOffset * -1, yOffset); + } +} diff --git a/src/lib/canvas/canvasUtils.ts b/src/lib/canvas/canvasUtils.ts new file mode 100644 index 00000000..0a55251e --- /dev/null +++ b/src/lib/canvas/canvasUtils.ts @@ -0,0 +1,44 @@ +export function drawCircle( + context: CanvasRenderingContext2D, + x: number, + y: number, + radius: number, + stroke?: string, + fill?: string | CanvasGradient, +) { + context.beginPath(); + context.arc(x, y, radius, 0, Math.PI * 2, false); + if (stroke) { + context.strokeStyle = stroke; + context.stroke(); + } + if (fill) { + context.fillStyle = fill; + context.fill(); + } + context.closePath(); +} + +export function drawLine( + context: CanvasRenderingContext2D, + startX: number, + startY: number, + endX: number, + endY: number, +) { + context.lineTo(startX, startY); + context.lineTo(endX, endY); +} + +export function drawLineAtAngle( + context: CanvasRenderingContext2D, + x: number, + y: number, + length: number, + angle: number, +) { + const offsetX = length * Math.cos(angle); + const offsetY = length * Math.sin(angle); + context.moveTo(x, y); + context.lineTo(x - offsetX, y - offsetY); +} diff --git a/src/lib/canvas/mountainScene.ts b/src/lib/canvas/mountainScene.ts new file mode 100644 index 00000000..543f60f2 --- /dev/null +++ b/src/lib/canvas/mountainScene.ts @@ -0,0 +1,257 @@ +import { Artist } from "./Artist"; +import { drawCircle } from "./canvasUtils"; +import { TREE_LINE_LEFT, TREE_LINE_RIGHT } from "../data/mountainSceneData"; + +export function mountainScene( + ctx: CanvasRenderingContext2D, + anCtx: CanvasRenderingContext2D, + width: number, + height: number, + percentage: number, +) { + const a = new Artist(ctx, 0, 0); + const SHADOW = "#715a74"; + const HIGHLIGHT = "#ce8eaa"; + const HILL_LEFT_FILL = "rgb(65, 63, 85)"; + const HILL_RIGHT_FILL = "rgb(74, 75, 96)"; + const TREE_FILL = "rgb(47, 48, 69)"; + const TREE_LINE = "rgb(71, 82, 104)"; + const STARS = "rgba(255, 255, 255, 0.2)"; + + const LEFT_HILL: Points = [ + [0, -height * 0.3], + [width * 0.6, height * 0.3], + ]; + + // intersection of height*0.3 - width * 0.6, + // widht * 0.5 and height * 0.55 + // y = mx + c + // mountain equation: y = (height * 0.55 / width * 0.5)x + // hill equation: y = -(height*0.3 / width * 0.6)x + height * 0.3 + + const RIGHT_HILL: Points = [ + [0, -height * 0.2], + [-width * 0.6, height * 0.2], + ]; + + const MOUNTAIN_SHADOW: Points = [ + [-width * 0.5, height * 0.55], + [width, 0], + [-width * 0.5, -height * 0.55], + ]; + + const MOUNTAIN_HIGHLIGHT: Points = [ + [width * 0.5, height * 0.55], + [-width * 0.5, 0], + [-width * 0.1, -height * 0.15], + [width * 0.1, height * 0.05], + [-width * 0.1, -height * 0.2], + [width * 0.1, height * 0.1], + [-width * 0.05, -height * 0.1], + [width * 0.1, height * 0.08], + [width * 0.1, height * 0.05], + [-width * 0.1, -height * 0.25], + [width * 0.08, height * 0.1], + ]; + + const TREE_CONE_LEFT: Points = [ + [width * 0.07, height * 0.03], + [-width * 0.035, -height * 0.25], + ]; + const TREE_CONE_LEFT_SMALLER: Points = [ + [width * 0.05, height * 0.025], + [-width * 0.025, -height * 0.2], + ]; + + const TREE_CONE_RIGHT: Points = [ + [width * 0.07, -height * 0.03], + [-width * 0.035, -height * 0.25], + ]; + const TREE_CONE_RIGHT_LARGER: Points = [ + [width * 0.08, -height * 0.025], + [-width * 0.04, -height * 0.35], + ]; + + const CLOUDS: Points = [ + [width, 0], + [0, height * 0.5], + [-width, 0], + ]; + + const cloudGradient = ctx.createLinearGradient(width * 0.5, height, width * 0.5, height * 0.6); + cloudGradient.addColorStop(0, "rgba(240,240,240,0.9"); + cloudGradient.addColorStop(0.2, "rgba(240,240,240,0.7"); + cloudGradient.addColorStop(0.8, "rgba(240,240,240,0.1"); + cloudGradient.addColorStop(1, "transparent"); + + const GLOW: Points = [ + [width * 0.5, 0], + [0, height * 0.7], + [-width * 0.5, 0], + ]; + + const glowGradient = ctx.createLinearGradient(width, height, width * 0.95, height * 0.35); + glowGradient.addColorStop(0, "rgb(246, 174, 156)"); + glowGradient.addColorStop(0.5, "rgb(160, 113, 133"); + glowGradient.addColorStop(1, "transparent"); + + // eslint-disable-next-line + function generateTreeLine(): Points { + const points: Points = [ + // [-width * 0.5, -height* 0.2] + [0, height * 0.2], + [-width * 0.5, 0], + ]; + let absX = 0; + let yDirection = 1; + let x = 0; + let y = 0; + while (absX < width * 0.5) { + x = Math.random() * width * 0.01; + yDirection *= -1; + y = yDirection * height * 0.05 * Math.random(); + points.push([x, y]); + absX += x; + } + + return points; + } + + a.reset(width * 0.5, height * 0.3); + a.drawShape(GLOW, false, 0, 0, 0, undefined, glowGradient); + + // Mountain + a.reset(width * 0.5, height * 0.45); + a.drawShape(MOUNTAIN_SHADOW, false, 0, 0, 0, SHADOW, SHADOW); + a.reset(width * 0.5, height * 0.45); + a.drawShape(MOUNTAIN_HIGHLIGHT, false, 0, 0, 0, HIGHLIGHT, HIGHLIGHT); + + // Clouds + a.reset(0, height * 0.5); + a.drawShape(CLOUDS, false, 0, 0, 0, undefined, cloudGradient); + + // Trees back + a.reset(width * 0.5, height * 0.96); + a.drawShape(TREE_LINE_LEFT, false, 0, 0, 0, TREE_LINE, TREE_LINE); + a.reset(width, height * 0.76); + a.drawShape(TREE_LINE_RIGHT, false, 0, 0, 0, TREE_LINE, TREE_LINE); + + // Hills + a.reset(0, height); + a.drawShape(LEFT_HILL, false, 0, 0, 0, HILL_LEFT_FILL, HILL_LEFT_FILL); + a.reset(width, height); + a.drawShape(RIGHT_HILL, false, 0, 0, 0, HILL_RIGHT_FILL, HILL_RIGHT_FILL); + + // Trees left + a.reset(-width * 0.01, height * 0.72); + a.drawShape(TREE_CONE_LEFT, false, 0, 0, 0, TREE_FILL, TREE_FILL); + a.reset(width * 0.04, height * 0.8); + a.drawShape(TREE_CONE_LEFT, false, 0, 0, 0, TREE_FILL, TREE_FILL); + a.reset(width * 0.14, height * 0.88); + a.drawShape(TREE_CONE_LEFT_SMALLER, false, 0, 0, 0, TREE_FILL, TREE_FILL); + + // Trees right + a.reset(width * 0.95, height * 0.9); + a.drawShape(TREE_CONE_RIGHT, false, 0, 0, 0, TREE_FILL, TREE_FILL); + a.reset(width * 0.86, height * 0.86); + a.drawShape(TREE_CONE_RIGHT, false, 0, 0, 0, TREE_FILL, TREE_FILL); + a.reset(width * 0.76, height * 0.95); + a.drawShape(TREE_CONE_RIGHT_LARGER, false, 0, 0, 0, TREE_FILL, TREE_FILL); + + function drawStar(x: number, y: number, stroke: string) { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x, y); + ctx.strokeStyle = stroke; + ctx.lineWidth = 4; + ctx.stroke(); + ctx.closePath(); + } + + drawStar(width * 0.1, height * 0.15, STARS); + drawStar(width * 0.23, height * 0.34, STARS); + drawStar(width * 0.37, height * 0.17, STARS); + drawStar(width * 0.66, height * 0.05, STARS); + drawStar(width * 0.13, height * 0.4, STARS); + drawStar(width * 0.72, height * 0.53, STARS); + drawStar(width * 0.81, height * 0.47, STARS); + drawStar(width * 0.92, height * 0.07, STARS); + + // Moon + const radius = width * 0.04; + const circle1 = [width * 0.5, height * 0.15]; + const circle2 = [circle1[0] - radius / 3, circle1[1] - radius / 3]; + + ctx.fillStyle = "white"; + ctx.beginPath(); + ctx.arc(circle1[0], circle1[1], radius, 0, Math.PI * 2, true); + ctx.fill(); + ctx.globalCompositeOperation = "destination-out"; + ctx.beginPath(); + ctx.arc(circle2[0], circle2[1], radius, 0, Math.PI * 2, true); + ctx.fill(); + ctx.closePath(); + + ctx.globalCompositeOperation = "source-over"; + + const xIntersect = 0.1875; + const yIntersect = 0.79375; + const intersection = [xIntersect * width, yIntersect * height]; + const adj = width * (0.5 - xIntersect); + const opp = height * (0.55 - (1 - yIntersect)); + + const indicatorRadius = width * 0.05; + const indicatorX = adj * percentage + intersection[0]; + const indicatorY = intersection[1] - opp * percentage; + + let indicatorGradient = anCtx.createRadialGradient( + indicatorX, + indicatorY, + indicatorRadius * 0.2, + indicatorX, + indicatorY, + indicatorRadius * 0.9, + ); + indicatorGradient.addColorStop(0, "rgb(240, 150, 80)"); + indicatorGradient.addColorStop(0.4, "rgba(240, 150, 80, 0.3)"); + indicatorGradient.addColorStop(1, "transparent"); + + anCtx.beginPath(); + drawCircle(anCtx, indicatorX, indicatorY, indicatorRadius, undefined, indicatorGradient); + + let counter = 0; + let degrees = 0; + let rad = 0; + let chance; + const clamp = 50; + function animate() { + counter++; + if (counter % 2 === 0) { + degrees = (degrees + 1) % (180 - clamp); + degrees = Math.max(clamp, degrees); + if (degrees === 110) { + chance = Math.random(); + if (chance > 0.4) { + degrees = 70; + } + } + rad = (degrees * Math.PI) / 180; + anCtx.clearRect(indicatorX - indicatorRadius, indicatorY - indicatorRadius, indicatorX, indicatorY); + indicatorGradient = ctx.createRadialGradient( + indicatorX, + indicatorY, + indicatorRadius * 0.2, + indicatorX, + indicatorY, + indicatorRadius * Math.sin(rad), + ); + indicatorGradient.addColorStop(0, "rgb(240, 150, 80)"); + indicatorGradient.addColorStop(0.3, "rgba(240, 150, 80, 0.3)"); + indicatorGradient.addColorStop(1, "transparent"); + drawCircle(anCtx, indicatorX, indicatorY, indicatorRadius, undefined, indicatorGradient); + } + requestAnimationFrame(animate); + } + + animate(); +} diff --git a/src/lib/canvas/weather/animate/Bluebody.ts b/src/lib/canvas/weather/animate/Bluebody.ts new file mode 100644 index 00000000..fd9fd219 --- /dev/null +++ b/src/lib/canvas/weather/animate/Bluebody.ts @@ -0,0 +1,83 @@ +import { Animator } from "$lib/canvas/Animator"; + +export class Bluebody extends Animator { + radialGradient: CanvasGradient; + orbitPosX: number; + orbitPosY: number; + orbitCenX: number; + orbitCenY: number; + orbitRadius: number; + orbitBodyRadius: number; + + constructor( + context: CanvasRenderingContext2D, + time: Date, + orbitCenX: number, + orbitCenY: number, + orbitRadius: number, + orbitBodyRadius: number, + ) { + super(context); + // ctx.beginPath(); + // ctx.arc( + // orbitCentreX, + // orbitCentreY, + // orbitRadius, + // Math.PI, Math.PI * 2, false); + // ctx.stroke(); + this.orbitPosX = 0; + this.orbitPosY = 0; + this.orbitCenY = orbitCenY; + this.orbitCenX = orbitCenX; + + this.orbitRadius = orbitRadius; + this.orbitBodyRadius = orbitBodyRadius; + this.updateTime(time); + this.radialGradient = context.createRadialGradient(0, 0, 0, 0, 0, 0); + this.ctx = context; + // sun or moon + + // context.fillRect(orbitPositionX, orbitPositionY, orbitBodyRadius * 2, orbitBodyRadius * 2); + } + + updateTime(time: Date) { + const hours = time.getHours(); + const moduloTime = hours + (6 % 24); + const orbitAngle = Math.PI - ((((2 * Math.PI) / 24) * moduloTime) % Math.PI) - Math.PI / 2; + const orbitLenX = Math.sin(orbitAngle) * this.orbitRadius; + const orbitLenY = Math.cos(orbitAngle) * this.orbitRadius; + this.orbitPosX = this.orbitCenX - orbitLenX; + this.orbitPosY = this.orbitCenY - orbitLenY; + if (hours >= 6 && hours < 18) { + this.radialGradient = this.getSunRadialGradient(this.ctx, this.orbitPosX, this.orbitPosY, this.orbitBodyRadius); + } else { + this.radialGradient = this.getMoonRadialGradient(this.ctx, this.orbitPosX, this.orbitPosY, this.orbitBodyRadius); + } + this.draw(); + + } + + animate = () => { + this.draw(); + }; + + draw = () => { + this.ctx.fillStyle = this.radialGradient; + this.ctx.arc(this.orbitPosX, this.orbitPosY, this.orbitBodyRadius, 0, 2 * Math.PI, false); + this.ctx.fill(); + }; + + getSunRadialGradient(context: CanvasRenderingContext2D, x: number, y: number, r: number) { + const radialGradient = context.createRadialGradient(x, y, r * 0.8, x, y, r); + radialGradient.addColorStop(0, "rgba(255, 240, 210, 1)"); + radialGradient.addColorStop(1, "rgba(255, 255, 0, 0)"); + return radialGradient; + } + + getMoonRadialGradient(context: CanvasRenderingContext2D, x: number, y: number, r: number) { + const radialGradient = context.createRadialGradient(x, y, r * 0.5, x, y, r * 0.8); + radialGradient.addColorStop(0, "rgba(255, 255, 255, 1)"); + radialGradient.addColorStop(1, "rgba(50, 50, 50, 0)"); + return radialGradient; + } +} diff --git a/src/lib/canvas/weather/animate/Cloud.ts b/src/lib/canvas/weather/animate/Cloud.ts new file mode 100644 index 00000000..fac26635 --- /dev/null +++ b/src/lib/canvas/weather/animate/Cloud.ts @@ -0,0 +1,95 @@ +import { Animator } from "$lib/canvas/Animator"; +import { Vector2D } from "$lib/mechanics/vector"; +import { randBetween } from "$lib/utils"; + +const TWO_PI = Math.PI * 2; +const ORB_SIZE_VARIANCE = [0.8, 1.2]; +const ORB_X_SPREAD_FACTOR = [-2, 2]; +const ORB_Y_SPREAD_FACTOR = [-0.5, 1]; + +export class Cloud extends Animator { + position: Vector2D; + speed: number; + orbs: Orb[] = []; + width: number; + radius: number; + size: number; + color: string; + + constructor(context: CanvasRenderingContext2D, x: number, y: number, radius: number, size: number, speed: number, color: string) { + super(context); + this.position = new Vector2D(x, y); + this.speed = speed; + this.size = size; + this.ctx = context; + this.width = 0; + this.color = color; + this.radius = radius; + } + + animate = () => { + this.position.x += this.speed; + // regenerate once cloud has left screen + if (this.position.x > this.ctx.canvas.width + this.width) { + this.generate(); + this.position.x = -this.width; + // vary height, keep within top half of canvas + this.position.y += randBetween(-this.width, this.width); + this.position.y = Math.max(0, this.position.y); + this.position.y = Math.min(this.ctx.canvas.height / 2, this.position.y); + } + this.draw(); + }; + + draw = () => { + this.orbs.forEach((orb) => orb.draw(this.ctx, this.position.x, this.position.y)); + }; + + generate = () => { + let smallestX = 0; + let largestX = 0; + + this.orbs.splice(0, this.orbs.length); + + for (let i = 0; i < this.size; i++) { + const orbRadius = randBetween(this.radius * ORB_SIZE_VARIANCE[0], this.radius * ORB_SIZE_VARIANCE[1]); + const orbX = randBetween(ORB_X_SPREAD_FACTOR[0] * this.radius, ORB_X_SPREAD_FACTOR[1] * this.radius); + const orbY = randBetween(ORB_Y_SPREAD_FACTOR[0] * this.radius, ORB_Y_SPREAD_FACTOR[1] * this.radius); + if (orbX < smallestX) { + smallestX = orbX; + } + if (orbX > largestX) { + largestX = orbX; + } + this.orbs.push(new Orb(orbX, orbY, orbRadius, this.color)); + } + + this.width = largestX - smallestX; + }; +} + +class Orb { + pos: Vector2D; + color: string; + radius: number; + + constructor(x: number, y: number, radius: number, color: string) { + this.color = color; + this.radius = radius; + this.pos = new Vector2D(x, y); + } + + draw = (ctx: CanvasRenderingContext2D, x: number, y: number) => { + const absX = x + this.pos.x; + const absY = y + this.pos.y; + + const radialGradient = ctx.createRadialGradient(absX, absY, 0, absX, absY, this.radius); + radialGradient.addColorStop(0, this.color); + radialGradient.addColorStop(1, "rgba(255, 255, 255, 0)"); + ctx.beginPath(); + ctx.fillStyle = radialGradient; + ctx.arc(absX, absY, this.radius, 0, TWO_PI, true); + ctx.fill(); + ctx.closePath(); + }; +} diff --git a/src/lib/canvas/weather/animate/Precipitater.ts b/src/lib/canvas/weather/animate/Precipitater.ts new file mode 100644 index 00000000..8f135511 --- /dev/null +++ b/src/lib/canvas/weather/animate/Precipitater.ts @@ -0,0 +1,58 @@ +import { Animator } from "$lib/canvas/Animator"; +import { Vector2D } from "$lib/mechanics/vector"; + +export abstract class Precipitator extends Animator { + canvasWidth: number; + canvasHeight: number; + drops: Drop[] = []; + dropSpeed: number; + windSpeed: number; + size: number; + + constructor(context: CanvasRenderingContext2D, speed: number, density: number, windSpeed: number, size: number) { + super(context); + this.canvasWidth = context.canvas.width; + this.canvasHeight = context.canvas.height; + this.dropSpeed = speed; + this.size = size; + this.windSpeed = windSpeed; + const dropSpacing = this.size / density; + const columns = Math.floor(this.canvasWidth / (this.size + dropSpacing)); + + for (let i = 0; i < columns; i++) { + this.drops.push(this.generateDrop(i * (dropSpacing + this.size), Math.random() * this.canvasHeight)); + } + } + + abstract generateDrop(x: number, y: number): Drop; + + animate = () => { + this.drops.forEach((drop) => { + drop.position.y += this.dropSpeed; + drop.position.x += this.windSpeed / 10; + + if (drop.position.y > this.canvasHeight) { + // vary starting position for variety of y position + drop.position.y = 0 - Math.random() * this.canvasHeight; + // shift drop position left or right up to drop width + drop.position.x = drop.position.x + (Math.random() * 2 - 1.0) * 3 * this.size; + } + if (drop.position.x > this.canvasWidth) { + drop.position.x = 0; + } + drop.draw(this.ctx); + }); + }; +} + +export abstract class Drop { + position: Vector2D; + size: number; + + constructor(size: number, initPos: Vector2D) { + this.position = initPos; + this.size = size; + } + + abstract draw(context: CanvasRenderingContext2D): void; +} diff --git a/src/lib/canvas/weather/animate/Rain.ts b/src/lib/canvas/weather/animate/Rain.ts new file mode 100644 index 00000000..fddbd049 --- /dev/null +++ b/src/lib/canvas/weather/animate/Rain.ts @@ -0,0 +1,37 @@ +import { Vector2D } from "$lib/mechanics/vector"; +import { Drop, Precipitator } from "./Precipitater"; + +const RAIN_COLOR = "rgb(90, 140, 210)"; +const DROP_SIZE = 10; + +export class Rain extends Precipitator { + constructor(context: CanvasRenderingContext2D, speed: number, density: number, windSpeed: number, size: number = DROP_SIZE) { + super(context, speed, density, windSpeed, size); + } + + generateDrop(x: number, y: number): Drop { + return new RainDrop(this.size, new Vector2D(x, y)); + } +} + +class RainDrop extends Drop { + height: number; + + constructor(size: number, initPos: Vector2D) { + super(size, initPos); + this.height = size * 2.5; + } + + draw(context: CanvasRenderingContext2D) { + context.beginPath(); + context.fillStyle = RAIN_COLOR; + context.moveTo(this.position.x, this.position.y); + context.lineTo(this.position.x - this.size, this.position.y + this.height); + context.lineTo(this.position.x + this.size, this.position.y + this.height); + context.lineTo(this.position.x, this.position.y); + context.fill(); + context.arc(this.position.x, this.position.y + this.height, this.size, 1.9 * Math.PI, 1.1 * Math.PI); + context.fill(); + context.closePath(); + } +} diff --git a/src/lib/canvas/weather/animate/Snow.ts b/src/lib/canvas/weather/animate/Snow.ts new file mode 100644 index 00000000..002e07b8 --- /dev/null +++ b/src/lib/canvas/weather/animate/Snow.ts @@ -0,0 +1,71 @@ +import { Vector2D } from "$lib/mechanics/vector"; +import { Drop, Precipitator } from "./Precipitater"; + +const SNOW_COLOR = "rgb(180, 190, 255)"; +// const FLAKE_SIZE = 40; +// const FLAKE_BRANCH_LENGTH = FLAKE_SIZE / 2; + +export class Snow extends Precipitator { + constructor(context: CanvasRenderingContext2D, speed: number, density: number, windSpeed: number, size: number) { + super(context, speed, density, windSpeed, size); + } + + generateDrop(x: number, y: number): Drop { + return new SnowFlake(this.size, new Vector2D(x, y)); + } + + draw = () => { + this.drops.forEach((drop) => drop.draw(this.ctx)); + }; +} + +class SnowFlake extends Drop { + constructor(size: number, initPos: Vector2D) { + super(size, initPos); + } + + draw(context: CanvasRenderingContext2D) { + context.beginPath(); + context.strokeStyle = SNOW_COLOR; + context.lineCap = "round"; + context.lineWidth = this.size / 20; + + this.drawBranch(context, this.size / 2, Math.PI / 6); + this.drawBranch(context, this.size / 2, Math.PI / 2); + this.drawBranch(context, this.size / 2, (5 * Math.PI) / 6); + this.drawBranch(context, this.size / 2, (7 * Math.PI) / 6); + this.drawBranch(context, this.size / 2, (3 * Math.PI) / 2); + this.drawBranch(context, this.size / 2, (11 * Math.PI) / 6); + + context.stroke(); + context.closePath(); + } + + drawBranch = (context: CanvasRenderingContext2D, radius: number, angle: number) => { + const offsetX = radius * Math.cos(angle); + const offsetY = radius * Math.sin(angle); + + // draw main branch + context.moveTo(this.position.x, this.position.y); + context.lineTo(this.position.x - offsetX, this.position.y - offsetY); + + const r2 = radius * 0.5; // length from origin to start of sub branch + const r3 = radius * 1.1; // length from + const subBranchAngle = Math.PI / 6; + const r4 = (r2 + r3) * Math.tan(subBranchAngle); + + const x2 = this.position.x - r2 * Math.cos(angle); + const y2 = this.position.y - r2 * Math.sin(angle); + + const x3 = this.position.x - r4 * Math.cos(subBranchAngle + angle); + const y3 = this.position.y - r4 * Math.sin(subBranchAngle + angle); + + const x4 = this.position.x - r4 * Math.cos(angle - subBranchAngle); + const y4 = this.position.y - r4 * Math.sin(angle - subBranchAngle); + + context.moveTo(x2, y2); + context.lineTo(x3, y3); + context.moveTo(x2, y2); + context.lineTo(x4, y4); + }; +} diff --git a/src/lib/canvas/weather/animate/Thunder.ts b/src/lib/canvas/weather/animate/Thunder.ts new file mode 100644 index 00000000..dcebde4b --- /dev/null +++ b/src/lib/canvas/weather/animate/Thunder.ts @@ -0,0 +1,55 @@ +import { Animator } from "$lib/canvas/Animator"; +import { randIntBetween } from "$lib/utils"; + +export class Thunder extends Animator { + frequency: number; + width: number; + height: number; + + countTo: number = 20; + count: number = 0; + flashCount: number = 0; + flashes: number = 2; + + constructor(context: CanvasRenderingContext2D, canvasHeight: number, canvasWidth: number, frequency: number) { + super(context); + this.frequency = frequency; + this.width = canvasWidth; + this.height = canvasHeight; + this.resetCounts(); + } + + animate() { + if (this.count >= this.countTo) { + if (this.flashCount <= this.flashes) { + if (this.flashCount % 8 == 0) { + this.drawFlash(); + } else if (this.flashCount % 4 == 0) { + this.drawBlank(); + } + this.flashCount++; + } else { + this.drawBlank(); + this.resetCounts(); + } + } else { + this.count++; + } + } + + resetCounts() { + this.count = 0; + this.flashCount = 0; + this.flashes = randIntBetween(4, 20); + this.countTo = randIntBetween(80, 400); + } + + drawFlash() { + this.ctx.fillStyle = "white"; + this.ctx.fillRect(0, 0, this.width, this.height); + } + drawBlank() { + this.ctx.fillStyle = "none"; + this.ctx.fillRect(0, 0, this.width, this.height); + } +} diff --git a/src/lib/canvas/weather/animate/utils.ts b/src/lib/canvas/weather/animate/utils.ts new file mode 100644 index 00000000..6d1bc116 --- /dev/null +++ b/src/lib/canvas/weather/animate/utils.ts @@ -0,0 +1,22 @@ +export class FrameRate { + + startTime: number = Date.now(); + totalElapsedTime: number = 0; + frames: number = 0; + frameRate: number = 0; + + calculateFrameRate() { + this.frames += 1; + this.totalElapsedTime = Date.now() - this.startTime; + this.frameRate = this.frames / this.totalElapsedTime * 1000; + } + + getFrameRate() { + return this.frameRate; + } + + getElapsedTime() { + return this.totalElapsedTime; + } + +} \ No newline at end of file diff --git a/src/lib/canvas/weather/draw/character.ts b/src/lib/canvas/weather/draw/character.ts new file mode 100644 index 00000000..8a4e1c12 --- /dev/null +++ b/src/lib/canvas/weather/draw/character.ts @@ -0,0 +1,247 @@ +import { Artist } from "../../Artist"; + +const AMBER = "rgb(245, 167, 66)"; +// const DARK_ORANGE = "rgb(200, 100, 100)"; +const WHITE = "white"; +const BLACK = "black"; +// const GREY = "rgb(50, 50, 50)"; +const ORANGE_BROWN = "rgb(220, 150, 110)"; +const SHADOW_BROWN = "rgb(120, 80, 20)"; +const DARK_SHADOW_BROWN = "rgb(71, 50, 44)"; +const DARK_GREY = "rgb(50, 50, 50)"; +const PINK = "rgb(220, 100, 150)"; +const DARK_PINK = "rgb(150, 50, 100)"; + +export function drawCharacter(context: CanvasRenderingContext2D, x: number, y: number) { + const a = new Artist(context, x, y); + + const HEAD_POINTS: Points = [ + // top + [15, 0], + // ear + [15, -5], + [20, 0], + [10, 10], + // side + [5, 20], + [0, 10], + [-10, 40], + [-25, 50], + // chin + [-10, 10], + [-10, 5], + [-10, 0], + ]; + + const HEAD_PATCH_POINTS: Points = [ + [10, 0], + [15, -3], + [20, 0], + [10, 35], + [-20, -15], + [-15, -8], + [-20, 0], + ]; + + const RIGHT_EAR_POINTS: Points = [ + // top arc + [5, -6], + [4, -2], + [5, -3], + [15, -5], + // down right + [35, 18], + [2, 10], + [-5, 20], + // up left + [-15, 30], + [-5, 5], + [-3, 0], + [-10, -5], + [-5, -10], + [-5, -10], + [5, -20], + [-5, -20], + ]; + + const LEFT_EAR_POINTS: Points = [ + // top arc + [-5, -10], + [-15, -5], + [-12, 9], + // down right + [-20, 3], + [-10, 5], + [-5, 5], + [15, 35], + // up left + [10, 20], + [10, 5], + [12, -20], + [0, -25], + [5, -10], + ]; + + const NOSE: Points = [ + [5, 0], + [6, 5], + [2, 9], + [-2, 4], + [-4, 2], + [-3, 0], + [-3, -1], + ]; + + const TONGUE: Points = [ + [10, 4], + [3, -2], + [-1, 10], + [-4, 5], + [-4, 3], + [-4, 0], + ]; + + a.drawShape(HEAD_POINTS, true, 0, 0, 2, ORANGE_BROWN, ORANGE_BROWN); + + a.reset(0, 10); + context.beginPath(); + a.drawNextLine(0, 35); + a.endX = a.startX + 20; + a.endY = a.startY + 35; + context.bezierCurveTo(a.startX, a.startY, a.startX, a.startY + 15, a.endX, a.endY); + context.lineCap = "round"; + context.lineWidth = 4; + context.strokeStyle = SHADOW_BROWN; + context.stroke(); + context.closePath(); + a.reset(0, 45); + context.beginPath(); + a.endX = a.startX - 20; + a.endY = a.startY + 35; + context.bezierCurveTo(a.startX, a.startY, a.startX, a.startY + 15, a.endX, a.endY); + context.lineCap = "round"; + context.lineWidth = 4; + context.strokeStyle = SHADOW_BROWN; + context.stroke(); + context.closePath(); + + a.reset(0, 3); + a.drawShape(HEAD_PATCH_POINTS, true, 0, 3, 2, DARK_SHADOW_BROWN, DARK_SHADOW_BROWN); + a.reset(25, -2); + a.drawShape(RIGHT_EAR_POINTS, false, 0, 0, 2, SHADOW_BROWN, SHADOW_BROWN); + a.reset(-25, 0); + a.drawShape(LEFT_EAR_POINTS, false, 0, 0, 2, SHADOW_BROWN, SHADOW_BROWN); + a.reset(0, 80); + a.drawShape(NOSE, true, 0, 80, 2, DARK_GREY, DARK_GREY); + drawEyes(context, x, y); + a.reset(0, 108); + a.drawShape(TONGUE, true, 0, 108, 2, DARK_PINK, PINK); + a.reset(45, 80); + drawFaceLines(context, a.startX, a.startY); +} + +function drawFaceLines(context: CanvasRenderingContext2D, x: number, y: number) { + context.beginPath(); + let startX = x; + let startY = y; + let endX = startX - 5; + let endY = startY + 5; + context.bezierCurveTo(startX, startY, startX - 3, startY - 3, endX, endY); + + startX = endX; + startY = endY; + endX = startX - 25; + endY = startY + 25; + context.bezierCurveTo(startX, startY, startX, startY + 10, endX, endY); + + startX = endX; + startY = endY; + endX = startX - 14; + endY = startY - 5; + context.bezierCurveTo(startX, startY, startX - 10, startY + 2, endX, endY); + + startX = endX; + startY = endY; + endX = startX - 14; + endY = startY + 5; + context.bezierCurveTo(startX, startY, startX - 4, startY + 7, endX, endY); + + startX = endX; + startY = endY; + endX = startX - 25; + endY = startY - 25; + context.bezierCurveTo(startX, startY, startX - 25, startY - 15, endX, endY); + + startX = endX; + startY = endY; + endX = startX - 5; + endY = startY - 5; + context.bezierCurveTo(startX, startY, startX - 2, startY - 7, endX, endY); + + context.lineCap = "round"; + context.lineWidth = 4; + context.strokeStyle = SHADOW_BROWN; + context.stroke(); + context.closePath(); +} + +function drawEyes(context: CanvasRenderingContext2D, x: number, y: number) { + const eyeSize = 17; + const eyePosX = 25; + const eyeTopPosY = 50; + const eyeBottomPosy = 34; + + // Right eye + context.beginPath(); + context.arc(x + eyePosX, y + eyeTopPosY, eyeSize, Math.PI * 1.2, Math.PI * 1.9); + context.fillStyle = DARK_GREY; + context.fill(); + context.closePath(); + context.beginPath(); + context.arc(x + eyePosX + 2, y + eyeBottomPosy, eyeSize, Math.PI * 2.2, Math.PI * 0.9); + context.fillStyle = DARK_GREY; + context.fill(); + + // Right pupil + context.beginPath(); + context.arc(x + eyePosX + 2, y + eyeTopPosY - 10, 7, Math.PI * 2, Math.PI * 1.1); + context.fillStyle = AMBER; + context.fill(); + + context.beginPath(); + context.arc(x + eyePosX + 2, y + eyeTopPosY - 8, 3, 0, Math.PI * 2); + context.fillStyle = BLACK; + context.fill(); + + context.beginPath(); + context.arc(x + eyePosX + 1, y + eyeTopPosY - 9, 1, 0, Math.PI * 2); + context.fillStyle = WHITE; + context.fill(); + + // Left eye + context.beginPath(); + context.arc(x - eyePosX, y + eyeTopPosY, eyeSize, Math.PI * 1.1, Math.PI * 1.8); + context.fillStyle = DARK_GREY; + context.fill(); + context.closePath(); + context.beginPath(); + context.arc(x - eyePosX - 2, y + eyeBottomPosy, eyeSize, Math.PI * 2.1, Math.PI * 0.8); + context.fillStyle = DARK_GREY; + context.fill(); + + // Left pupil + context.beginPath(); + context.arc(x - eyePosX - 2, y + eyeTopPosY - 10, 7, Math.PI * 1.9, Math.PI * 1); + context.fillStyle = AMBER; + context.fill(); + + context.beginPath(); + context.arc(x - eyePosX - 2, y + eyeTopPosY - 8, 3, 0, Math.PI * 2); + context.fillStyle = BLACK; + context.fill(); + + context.beginPath(); + context.arc(x - eyePosX - 3, y + eyeTopPosY - 9, 1, 0, Math.PI * 2); + context.fillStyle = WHITE; + context.fill(); +} diff --git a/src/lib/canvas/weather/draw/tree.ts b/src/lib/canvas/weather/draw/tree.ts new file mode 100644 index 00000000..21f12bfb --- /dev/null +++ b/src/lib/canvas/weather/draw/tree.ts @@ -0,0 +1,47 @@ +import { randRangeRGBString } from "$lib/utils"; + +// inspired by +// https://github.com/PavlyukVadim/amadev/blob/master/RecursiveTree/script.js +export function drawTree( + context: CanvasRenderingContext2D, + startX: number, + startY: number, + length: number, + angle: number, + depth: number, + branchWidth: number, +) { + let newLength, newAngle; + const rand = Math.random; + const maxAngle = (2 * Math.PI) / 6; + const maxBranch = 3; + const endX = startX + length * Math.cos(angle); + const endY = startY + length * Math.sin(angle); + + context.beginPath(); + context.moveTo(startX, startY); + context.lineCap = "round"; + context.lineWidth = branchWidth; + context.lineTo(endX, endY); + + if (depth <= 4) { + context.strokeStyle = randRangeRGBString(30, [64, 180], 0); + } else { + context.strokeStyle = randRangeRGBString([70, 80], 60, [30, 50]); + } + + context.stroke(); + const newDepth = depth - 1; + + if (!newDepth) { + return; + } + const subBranches = rand() * (maxBranch - 1) + 1; + branchWidth *= 0.7; + + for (let i = 0; i < subBranches; i++) { + newAngle = angle + rand() * maxAngle - maxAngle * 0.5; + newLength = length * (0.7 + rand() * 0.3); + drawTree(context, endX, endY, newLength, newAngle, newDepth, branchWidth); + } +} diff --git a/src/lib/canvas/weather/weatherTime.ts b/src/lib/canvas/weather/weatherTime.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/lib/canvas/wordSoup.ts b/src/lib/canvas/wordSoup.ts new file mode 100644 index 00000000..479432ba --- /dev/null +++ b/src/lib/canvas/wordSoup.ts @@ -0,0 +1,260 @@ +import { LANG_LOGO_COLORS } from "../data/codeLangData.ts"; +import { + resolveCollisions, + type Rect, + resolveCollision, +} from "../mechanics/rect"; +import { Vector2D, randomVector } from "../mechanics/vector"; + +export const DEFAULT_GRAVITY = new Vector2D(0, 0.2); +const DEFAULT_DAMPING_FACTOR = 0.3; +const DEFAULT_MIN_MAGNITUDE = 4.8; +const DEFAULT_FONT_AREA = 1200; +const DEFAULT_MIN_FONT = 32; + +const COLORS = ["blue", "red", "green", "purple", "orange", "magenta", "lime"]; + +function getRects(wordBlocks: WordBlock[]): Rect[] { + return wordBlocks.map((block) => block.getRect()); +} + +function getColor(technology: string) { + if (LANG_LOGO_COLORS[technology]) { + return LANG_LOGO_COLORS[technology]; + } else { + const randomIndex = Math.floor(Math.random() * COLORS.length); + return COLORS[randomIndex]; + } +} + +function getProportionalFontSize( + fontArea: number | undefined, + value: number, + minFontSize?: number, +) { + const result = (fontArea ?? DEFAULT_FONT_AREA) * value; + return Math.max(result, minFontSize ?? DEFAULT_MIN_FONT); +} + +export class WordSoup { + wordBlocks: WordBlock[] = []; + ctx: CanvasRenderingContext2D; + canvas: HTMLCanvasElement; + counter: number = 0; + randomForces: boolean = false; + gravity: Vector2D = DEFAULT_GRAVITY; + dampingFactor: number = DEFAULT_DAMPING_FACTOR; + + constructor( + canvas: HTMLCanvasElement, + context: CanvasRenderingContext2D, + words: Record, + fontArea?: number, + minFontSize?: number, + ) { + this.canvas = canvas; + this.ctx = context; + this.ctx.textAlign = "left"; + this.ctx.textBaseline = "top"; + + Object.keys(words).forEach((word) => { + this.wordBlocks.push( + new WordBlock( + word, + getProportionalFontSize(fontArea, words[word], minFontSize), + getColor(word), + ), + ); + }); + + let lastPlacedBlock: WordBlock; + const nextPosition = new Vector2D(10, 10); + const maxBlockHeight = Math.max(...this.wordBlocks.map((b) => b.height)); + this.wordBlocks.forEach((block) => { + if (lastPlacedBlock) { + nextPosition.x += + lastPlacedBlock.width + Math.random() * lastPlacedBlock.width; + if (nextPosition.x + block.width > canvas.width) { + nextPosition.x = 10 + Math.random() * 10; + nextPosition.y += + maxBlockHeight * 2 + 10 + Math.random() * maxBlockHeight; + } + } + + block.position.x = nextPosition.x; + block.position.y = nextPosition.y; + lastPlacedBlock = block; + block.draw(this.ctx); + }); + } + + setForces( + value: Vector2D, + randomForces: boolean = false, + dampingFactor?: number, + ) { + this.randomForces = randomForces; + this.setGravity(value); + if (dampingFactor) { + this.dampingFactor = dampingFactor; + } + } + + setGravity(value: Vector2D) { + this.gravity = value; + } + + animate = () => { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + const rects = getRects(this.wordBlocks); + this.wordBlocks.forEach((block) => { + const force = this.randomForces ? randomVector(1, 2) : this.gravity; + this.updateVelocity(block, rects, force); + block.draw(this.ctx); + }); + requestAnimationFrame(this.animate); + }; + + updateVelocity(word: WordBlock, collisionRects: Rect[], force: Vector2D) { + word.velocity.add(force); + + const potentialPos = new Vector2D( + word.position.x + word.velocity.x, + word.position.y + word.velocity.y, + ); + + const potentialRect: Rect = { + x1: potentialPos.x, + x2: potentialPos.x + word.width, + y1: potentialPos.y, + y2: potentialPos.y + word.height, + }; + + const collision = resolveCollisions( + potentialRect, + word.getRect(), + collisionRects, + word.velocity, + ); + + let rectangleCollision = false; + let boundaryCollision = false; + + if (collision) { + rectangleCollision = true; + word.velocity.add(collision.response); + } + + if ( + potentialPos.x < 0 || + potentialPos.x >= this.canvas.width - word.width || + (collision && Math.abs(collision.response.x) > 0) + ) { + word.velocity.x = word.velocity.x * -1; + boundaryCollision = true; + } + + if ( + potentialPos.y < 0 || + potentialPos.y >= this.canvas.height - word.height || + (collision && Math.abs(collision.response.y) > 0) + ) { + word.velocity.y = word.velocity.y * -1; + boundaryCollision = true; + } + + if (boundaryCollision) { + word.velocity.attenuate(this.dampingFactor, DEFAULT_MIN_MAGNITUDE); + } + + word.position.add(word.velocity); + + if (rectangleCollision && collision) { + const response = resolveCollision( + word.getRect(), + collisionRects[collision.index], + word.velocity, + ); + if (response) { + word.position.add(response); + } + } + + if (word.position.x < 0) { + word.position.x = 1; + } + + if (word.position.x >= this.canvas.width - word.width) { + word.position.x = this.canvas.width - word.width - 1; + } + + if (word.position.y < 0) { + word.position.y = 1; + } + if (word.position.y >= this.canvas.height - word.height) { + word.position.y = this.canvas.height - word.height - 1; + } + } +} + +class WordBlock { + height: number; + width: number; + color: string; + word: string; + charSize: number; + velocity: Vector2D; + position: Vector2D = new Vector2D(0, 0); + rotationMomentum: number = 0; + + constructor(word: string, charSize: number, color: string) { + this.height = charSize + 8; + this.width = charSize * word.length * 0.6 + 16; + this.color = color; + this.word = word; + this.charSize = charSize; + this.velocity = new Vector2D(0, 0); + } + + draw(ctx: CanvasRenderingContext2D) { + this.position.round(); + ctx.beginPath(); + // ctx.fillRect(this.position.x, this.position.y, this.width, this.height); + ctx.roundRect( + this.position.x, + this.position.y, + this.width, + this.height, + 12, + ); + ctx.fillStyle = this.color; + ctx.fill(); + ctx.closePath(); + + ctx.beginPath(); + ctx.font = `bold ${this.charSize}px 'JetBrainsMono'`; + ctx.fillStyle = "white"; + ctx.fillText( + this.word, + this.position.x + 4, + this.position.y + 8, + this.width, + ); + ctx.fill(); + ctx.closePath(); + } + + setRandomPosition(bounds: Vector2D) { + this.position.x = Math.floor(Math.random() * (bounds.x - this.width)); + this.position.y = Math.floor(Math.random() * (bounds.y - this.height)); + } + + getRect(): Rect { + return { + x1: this.position.x, + x2: this.position.x + this.width, + y1: this.position.y, + y2: this.position.y + this.height, + }; + } +} diff --git a/src/lib/canvasUtils.ts b/src/lib/canvasUtils.ts deleted file mode 100644 index 975b0a66..00000000 --- a/src/lib/canvasUtils.ts +++ /dev/null @@ -1,21 +0,0 @@ -export function drawCircle(context: CanvasRenderingContext2D, - x: number, y: number, radius: number, stroke?: string, fill?: string | CanvasGradient) { - context.beginPath(); - context.arc(x, y, radius, 0, Math.PI *2, false); - if (stroke) { - context.strokeStyle = stroke; - context.stroke(); - } - if (fill) { - context.fillStyle = fill; - context.fill(); - } - context.closePath(); -} - -export function drawLine(context: CanvasRenderingContext2D, - startX: number, startY: number, endX: number, endY: number) { - context.lineTo(startX, startY); - context.lineTo(endX, endY); -} - diff --git a/src/lib/components/BackButton.svelte b/src/lib/components/BackButton.svelte index f2e0e13b..872a4092 100644 --- a/src/lib/components/BackButton.svelte +++ b/src/lib/components/BackButton.svelte @@ -1,31 +1,35 @@ - {isHovered = true;}} -on:mouseleave={() => {isHovered = false;}}> - + { + isHovered = true; + }} + on:mouseleave={() => { + isHovered = false; + }} +> + - \ No newline at end of file + a:hover { + text-decoration: none; + } + diff --git a/src/lib/components/BigIconButton.svelte b/src/lib/components/BigIconButton.svelte new file mode 100644 index 00000000..7d171458 --- /dev/null +++ b/src/lib/components/BigIconButton.svelte @@ -0,0 +1,48 @@ + + + + + diff --git a/src/lib/components/MountainCanvas.svelte b/src/lib/components/MountainCanvas.svelte index e54233c4..83e9c45b 100644 --- a/src/lib/components/MountainCanvas.svelte +++ b/src/lib/components/MountainCanvas.svelte @@ -1,61 +1,47 @@
- - + +
\ No newline at end of file + #mountain-canvas-container { + position: relative; + width: 100%; + height: var(--canvas-height); + } + + canvas { + width: 100%; + position: absolute; + top: 0; + left: 0; + border-radius: 12px; + } + + .scene-canvas { + background-color: rgb(38, 42, 71); + background: linear-gradient(176deg, rgb(23, 27, 51), rgb(38, 42, 71), rgb(94, 80, 117), rgb(163, 116, 139), rgb(234, 176, 156)); + } + diff --git a/src/lib/components/PostPreview.svelte b/src/lib/components/PostPreview.svelte index e0dc091e..2a4da3d5 100644 --- a/src/lib/components/PostPreview.svelte +++ b/src/lib/components/PostPreview.svelte @@ -1,63 +1,62 @@ -
-
- {#if hasPostImage} - - {/if} -
- {#if !isPinnedPost} -
{date}
- {/if} - {post.title} -
-
- {#if hasProjectLink} - - {/if} +
+ {#if hasPostImage} + + {/if} +
+ {#if !isPinnedPost} +
{date}
+ {/if} + {post.title} +
+
+ {#if hasProjectLink} + + {/if}
\ No newline at end of file + .project-link-container { + margin-left: auto; + margin-right: var(--margin); + color: var(--highlight); + display: flex; + justify-content: center; + align-items: center; + gap: 1em; + text-transform: uppercase; + } + diff --git a/src/lib/components/PostPreviewList.svelte b/src/lib/components/PostPreviewList.svelte index e19634eb..6b176803 100644 --- a/src/lib/components/PostPreviewList.svelte +++ b/src/lib/components/PostPreviewList.svelte @@ -1,16 +1,16 @@
- + {#each sortedPosts as post} - + {/each}
@@ -21,4 +21,4 @@ display: flex; flex-direction: column; } - \ No newline at end of file + diff --git a/src/lib/components/StravaGoalStats.svelte b/src/lib/components/StravaGoalStats.svelte index e4593173..af4c8e1c 100644 --- a/src/lib/components/StravaGoalStats.svelte +++ b/src/lib/components/StravaGoalStats.svelte @@ -1,77 +1,77 @@
-
-
{goalYear} Goal: {goal}
-
-
{current}m
-
- -
-
-
- {percentage.toFixed(0)}% - {#if !goalComplete} -
- ({onTrack ? "+" : ""}{schedule.toFixed(1)}% {onTrack ? "ahead" : "behind"}) -
- {/if} -
-
last updated: {formatDate(lastUpdated)}
-
-
- -
+
+
{goalYear} Goal: {goal}
+
+
{current}m
+
+ +
+
+
+ {percentage.toFixed(0)}% + {#if !goalComplete} +
+ ({onTrack ? "+" : ""}{schedule.toFixed(1)}% {onTrack ? "ahead" : "behind"}) +
+ {/if} +
+
last updated: {formatDate(lastUpdated)}
+
+
+ +
\ No newline at end of file + .badge { + margin-left: auto; + } + diff --git a/src/lib/components/TechnologySoup.svelte b/src/lib/components/TechnologySoup.svelte index ee8ad2be..18a160b1 100644 --- a/src/lib/components/TechnologySoup.svelte +++ b/src/lib/components/TechnologySoup.svelte @@ -1,6 +1,6 @@ + + diff --git a/src/lib/components/weather/WeatherControls.svelte b/src/lib/components/weather/WeatherControls.svelte new file mode 100644 index 00000000..ac5eeef2 --- /dev/null +++ b/src/lib/components/weather/WeatherControls.svelte @@ -0,0 +1,31 @@ + + +
+
+ {#each WEATHERS as weather} + setCurrentWeather(weather)} /> + {/each} +
+
+ + diff --git a/src/lib/config.ts b/src/lib/config/config.ts similarity index 100% rename from src/lib/config.ts rename to src/lib/config/config.ts diff --git a/src/lib/data/weatherData.ts b/src/lib/data/weatherData.ts new file mode 100644 index 00000000..36d59158 --- /dev/null +++ b/src/lib/data/weatherData.ts @@ -0,0 +1,92 @@ +export enum Weather { + Clear = "Clear", + Cloudy = "Cloudy", + Overcast = "Overcast", + Fog = "Fog", + Drizzle = "Drizzle", + Rain = "Rain", + Snow = "Snow", + Thunder = "Thunder", +} + +export const WEATHERS = Object.entries(Weather).map((w) => w[1]); + +export enum Direction { + N = "North", + NE = "North East", + E = "East", + SE = "South East", + S = "South", + SW = "South West", + W = "West", + NW = "North West", +} + +export interface WeatherAttributes { + name: string; + icon: string; + color: string; +} + +export const WEATHER_ATTRIBUTES: Record = { + Clear: { name: "Clear", color: "gold", icon: "sun" }, + Cloudy: { name: "Cloudy", color: "lightgray", icon: "cloud" }, + Overcast: { name: "Overcast", color: "gray", icon: "cloud" }, + Fog: { name: "Fog", color: "gray", icon: "smog" }, + Drizzle: { name: "Drizzle", color: "lightblue", icon: "cloud-rain" }, + Rain: { name: "Rain", color: "dodgerblue", icon: "cloud-rain" }, + Snow: { name: "Snow", color: "aliceblue", icon: "snowflake" }, + Thunder: { name: "Thunder", color: "darkgray", icon: "cloud-bolt" }, +}; + +/* +Code Description +0 Clear sky +1, 2, 3 Mainly clear, partly cloudy, and overcast +45, 48 Fog and depositing rime fog +51, 53, 55 Drizzle: Light, moderate, and dense intensity +56, 57 Freezing Drizzle: Light and dense intensity +61, 63, 65 Rain: Slight, moderate and heavy intensity +66, 67 Freezing Rain: Light and heavy intensity +71, 73, 75 Snow fall: Slight, moderate, and heavy intensity +77 Snow grains +80, 81, 82 Rain showers: Slight, moderate, and violent +85, 86 Snow showers slight and heavy +95 * Thunderstorm: Slight or moderate +96, 99 * Thunderstorm with slight and heavy hail +*/ + +export function getWeatherFromCode(code: number): Weather { + switch (code) { + case 0: + return Weather.Clear; + case 1 | 2: + return Weather.Cloudy; + case 3: + return Weather.Overcast; + case 45 | 48: + return Weather.Fog; + case 51 | 53 | 55 | 56 | 57: + return Weather.Drizzle; + case 61 | 63 | 65 | 66 | 67 | 80 | 81 | 82: + return Weather.Rain; + case 71 | 73 | 75 | 77 | 85 | 86: + return Weather.Snow; + case 95 | 96 | 99: + return Weather.Thunder; + default: + return Weather.Cloudy; + } +} + +export function getDirectionFromAngle(angle: number): Direction { + if (angle >= 337.5 || angle < 22.5) return Direction.N; + if (angle >= 22.5 && angle < 67.5) return Direction.NE; + if (angle >= 67.5 && angle < 112.5) return Direction.E; + if (angle >= 112.5 && angle < 157.5) return Direction.SE; + if (angle >= 157.5 && angle < 202.5) return Direction.S; + if (angle >= 202.5 && angle < 247.5) return Direction.SW; + if (angle >= 247.5 && angle < 292.5) return Direction.W; + if (angle >= 292.5 && angle < 337.5) return Direction.NW; + return Direction.N; +} diff --git a/src/lib/githubApi.ts b/src/lib/githubApi.ts deleted file mode 100644 index de80c056..00000000 --- a/src/lib/githubApi.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { error } from '@sveltejs/kit'; -import { EXTERNAL_CODE_LANGS, getLangShortName } from './data/codeLangData'; - -export async function ghGet(url: string, token: string) { - return await fetch(url, { - method: "GET", - mode: "cors", - credentials: "same-origin", - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${token}`, - "X-GitHub-Api-Version": "2022-11-28" - }, - } - ); -} - -async function extractLanguageData(data: JSONArray, token: string) { - try { - const urls: string[] = data.flatMap(repo => repo.languages_url); - const collated: Record[] = []; - await Promise.all(urls.map(async(url: string) => { - const response = await ghGet(url, token); - const data = await response.json() as Record; - if (data) { - collated.push(data); - } - })); - return parseLanguageObject(collated); - } catch { - error(500, "Error parsing coding language data from github") - } -} - -export async function getGhLanguageData(url: string, token: string) { - try { - const response = await ghGet(url, token); - const data = await response.json() as JSONArray; - const languages = await extractLanguageData(data, token); - return languages; - } catch { - error(500, "Error retrieving data from github") - } -} - -export function parseLanguageObject(data: Record[]): Record { - let total = 0; - const parsed: Record = {} - for (const languages of data) { - for (const language of Object.keys(languages)) { - const lang = getLangShortName(language); - if (parsed[lang]) { - // use proportional lines of code intead - // parsed[lang] += parsed[key]; - parsed[lang] += 1; - } else { - // use proportional lines of code intead - // parsed[lang] = parsed[key]; - parsed[lang] = 1; - } - // total += parsed[key]; - total += 1 - } - } - - for (const language of Object.keys(EXTERNAL_CODE_LANGS)) { - const lang = getLangShortName(language); - if (parsed[lang]) { - parsed[lang] += EXTERNAL_CODE_LANGS[language]; - } else { - parsed[lang] = EXTERNAL_CODE_LANGS[language]; - } - total += EXTERNAL_CODE_LANGS[language] - } - - for (const language of Object.keys(parsed)) { - parsed[language] = parsed[language] / total; - } - return parsed; -} diff --git a/src/lib/mechanics/vector.ts b/src/lib/mechanics/vector.ts index bb1ad267..2b93ed31 100644 --- a/src/lib/mechanics/vector.ts +++ b/src/lib/mechanics/vector.ts @@ -1,70 +1,70 @@ import { randomDirection, randomMinMax } from "../utils"; export class Vector2D { - x: number - y: number + x: number; + y: number; - constructor(x: number, y: number) { - this.x = x; - this.y = y; - } + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } - add(vector: Vector2D) { - this.x += vector.x; - this.y += vector.y; - } + add(vector: Vector2D) { + this.x += vector.x; + this.y += vector.y; + } - scale(factor: number) { - this.x *= factor; - this.y *= factor; - } + scale(factor: number) { + this.x *= factor; + this.y *= factor; + } - round() { - this.x = Math.floor(this.x); - this.y = Math.floor(this.y); - } + round() { + this.x = Math.floor(this.x); + this.y = Math.floor(this.y); + } - ZERO() { - this.x = 0; - this.y = 0; - } + ZERO() { + this.x = 0; + this.y = 0; + } - isZero() { - return this.x === 0 && this.y === 0; - } + isZero() { + return this.x === 0 && this.y === 0; + } - attenuate(factor: number, minMagnitude: number) { - const attenuatedX = this.x * factor; - const attenuatedY = this.y * factor; - this.x = Math.abs(attenuatedX) < minMagnitude ? 0 : attenuatedX; - this.y = Math.abs(attenuatedY) < minMagnitude ? 0 : attenuatedY; - } + attenuate(factor: number, minMagnitude: number) { + const attenuatedX = this.x * factor; + const attenuatedY = this.y * factor; + this.x = Math.abs(attenuatedX) < minMagnitude ? 0 : attenuatedX; + this.y = Math.abs(attenuatedY) < minMagnitude ? 0 : attenuatedY; + } - reverse() { - return new Vector2D(this.x * -1, this.y * -1); - } + reverse() { + return new Vector2D(this.x * -1, this.y * -1); + } } export function dotProduct(vector1: Vector2D, vector2: Vector2D): number { - return (vector1.x * vector2.x) + (vector1.y * vector2.y); + return vector1.x * vector2.x + vector1.y * vector2.y; } export function vectorMagnitude(vector: Vector2D): number { - return Math.sqrt(Math.pow(vector.x, 2) + Math.pow(vector.y, 2)); + return Math.sqrt(Math.pow(vector.x, 2) + Math.pow(vector.y, 2)); } export function reflectionVector(incident: Vector2D, normal: Vector2D) { - const product = dotProduct(incident, normal); - const sqrMagnitude = Math.pow(vectorMagnitude(normal), 2); - const x = incident.x + ((-2 * normal.x * product) / sqrMagnitude); - const y = incident.y + ((-2 * normal.y * product) / sqrMagnitude); - return new Vector2D(x, y); + const product = dotProduct(incident, normal); + const sqrMagnitude = Math.pow(vectorMagnitude(normal), 2); + const x = incident.x + (-2 * normal.x * product) / sqrMagnitude; + const y = incident.y + (-2 * normal.y * product) / sqrMagnitude; + return new Vector2D(x, y); } export function randomVector(min: number, max: number): Vector2D { - const x = randomMinMax(min, max) * randomDirection(); - const y = randomMinMax(min, max) * randomDirection(); - return new Vector2D(x, y); + const x = randomMinMax(min, max) * randomDirection(); + const y = randomMinMax(min, max) * randomDirection(); + return new Vector2D(x, y); } -export const ZERO_VECTOR = new Vector2D(0, 0); \ No newline at end of file +export const ZERO_VECTOR = new Vector2D(0, 0); diff --git a/src/lib/mountainScene.ts b/src/lib/mountainScene.ts deleted file mode 100644 index 34a0c723..00000000 --- a/src/lib/mountainScene.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { Artist } from "../Artist"; -import { drawCircle } from "./canvasUtils"; -import { TREE_LINE_LEFT, TREE_LINE_RIGHT } from "./data/mountainSceneData"; - -export function mountainScene(ctx: CanvasRenderingContext2D, anCtx: CanvasRenderingContext2D, width: number, height: number, percentage: number) { - - const a = new Artist(ctx, 0, 0); - const SHADOW = "#715a74"; - const HIGHLIGHT = "#ce8eaa"; - const HILL_LEFT_FILL = "rgb(65, 63, 85)"; - const HILL_RIGHT_FILL = "rgb(74, 75, 96)"; - const TREE_FILL = "rgb(47, 48, 69)"; - const TREE_LINE = "rgb(71, 82, 104)"; - const STARS = "rgba(255, 255, 255, 0.2)"; - - const LEFT_HILL: Points = [ - [0, -height * 0.3 ], - [width * 0.6, height * 0.3 ], - ]; - - // intersection of height*0.3 - width * 0.6, - // widht * 0.5 and height * 0.55 - // y = mx + c - // mountain equation: y = (height * 0.55 / width * 0.5)x - // hill equation: y = -(height*0.3 / width * 0.6)x + height * 0.3 - - - - const RIGHT_HILL: Points = [ - [0, -height * 0.2], - [-width * 0.6, height * 0.2 ], - ]; - - const MOUNTAIN_SHADOW: Points = [ - [-width * 0.5, height * 0.55], - [width, 0], - [-width * 0.5, -height * 0.55 ], - ]; - - const MOUNTAIN_HIGHLIGHT: Points = [ - [width * 0.5, height * 0.55], - [-width * 0.5, 0], - [-width * 0.1, - height * 0.15], - [width * 0.1, height * 0.05], - [-width * 0.1, - height * 0.2], - [width * 0.1, height * 0.1], - [-width * 0.05, - height * 0.1], - [width * 0.1, height * 0.08], - [width * 0.1, height * 0.05], - [-width * 0.1, -height * 0.25], - [width * 0.08, height * 0.1] - ]; - - const TREE_CONE_LEFT: Points = [ - [width * 0.07, height * 0.03], - [-width * 0.035, -height * 0.25], - ]; - const TREE_CONE_LEFT_SMALLER: Points = [ - [width * 0.05, height * 0.025], - [-width * 0.025, -height * 0.2], - ]; - - const TREE_CONE_RIGHT: Points = [ - [width * 0.07, -height * 0.03], - [-width * 0.035, -height * 0.25], - ]; - const TREE_CONE_RIGHT_LARGER: Points = [ - [width * 0.08, -height * 0.025], - [-width * 0.04, -height * 0.35], - ]; - - const CLOUDS: Points = [ - [width, 0], - [0, height * 0.5], - [-width, 0], - ]; - - const cloudGradient = ctx.createLinearGradient( - width * 0.5, height, - width * 0.5, height * 0.6); - cloudGradient.addColorStop(0, "rgba(240,240,240,0.9"); - cloudGradient.addColorStop(0.2, "rgba(240,240,240,0.7"); - cloudGradient.addColorStop(0.8, "rgba(240,240,240,0.1"); - cloudGradient.addColorStop(1, "transparent"); - - const GLOW: Points = [ - [width * 0.5, 0], - [0, height * 0.7], - [-width * 0.5, 0], - ] - - const glowGradient = ctx.createLinearGradient( - width, height, - width * 0.95, height * 0.35); - glowGradient.addColorStop(0, "rgb(246, 174, 156)"); - glowGradient.addColorStop(0.5, "rgb(160, 113, 133"); - glowGradient.addColorStop(1, "transparent"); - - // eslint-disable-next-line - function generateTreeLine(): Points { - const points: Points = [ - // [-width * 0.5, -height* 0.2] - [0, height * 0.2], - [-width * 0.5, 0], - ]; - let absX = 0; - let yDirection = 1; - let x = 0; - let y = 0; - while(absX < width * 0.5) { - x = Math.random() * width * 0.01; - yDirection *= -1; - y = yDirection * height * 0.05 * Math.random(); - points.push([x, y]); - absX += x; - } - - return points; - } - - a.reset(width * 0.5, height * 0.3) - a.drawShape(GLOW, false, 0, 0, 0, undefined, glowGradient); - - // Mountain - a.reset(width * 0.5, height * 0.45); - a.drawShape(MOUNTAIN_SHADOW, false, 0, 0, 0, SHADOW, SHADOW); - a.reset(width * 0.5, height * 0.45); - a.drawShape(MOUNTAIN_HIGHLIGHT, false, 0, 0, 0, HIGHLIGHT, HIGHLIGHT); - - // Clouds - a.reset(0, height * 0.5) - a.drawShape(CLOUDS, false, 0, 0, 0, undefined, cloudGradient); - - // Trees back - a.reset(width * 0.5, height * 0.96) - a.drawShape(TREE_LINE_LEFT, false, 0, 0, 0, TREE_LINE, TREE_LINE); - a.reset(width, height * 0.76) - a.drawShape(TREE_LINE_RIGHT, false, 0, 0, 0, TREE_LINE, TREE_LINE); - - // Hills - a.reset(0, height); - a.drawShape(LEFT_HILL, false, 0, 0, 0, HILL_LEFT_FILL, HILL_LEFT_FILL) - a.reset(width, height); - a.drawShape(RIGHT_HILL, false, 0, 0, 0, HILL_RIGHT_FILL, HILL_RIGHT_FILL) - - // Trees left - a.reset(-width * 0.01, height * 0.72); - a.drawShape(TREE_CONE_LEFT, false, 0, 0, 0, TREE_FILL, TREE_FILL) - a.reset(width * 0.04, height * 0.80); - a.drawShape(TREE_CONE_LEFT, false, 0, 0, 0, TREE_FILL, TREE_FILL) - a.reset(width * 0.14, height * 0.88); - a.drawShape(TREE_CONE_LEFT_SMALLER, false, 0, 0, 0, TREE_FILL, TREE_FILL) - - // Trees right - a.reset(width * 0.95, height * 0.9); - a.drawShape(TREE_CONE_RIGHT, false, 0, 0, 0, TREE_FILL, TREE_FILL) - a.reset(width * 0.86, height * 0.86); - a.drawShape(TREE_CONE_RIGHT, false, 0, 0, 0, TREE_FILL, TREE_FILL) - a.reset(width * 0.76, height * 0.95); - a.drawShape(TREE_CONE_RIGHT_LARGER, false, 0, 0, 0, TREE_FILL, TREE_FILL) - - function drawStar(x: number, y: number, stroke: string) { - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(x, y); - ctx.strokeStyle = stroke; - ctx.lineWidth = 4; - ctx.stroke(); - ctx.closePath(); - } - - drawStar(width * 0.1, height * 0.15, STARS); - drawStar(width * 0.23, height * 0.34, STARS); - drawStar(width * 0.37, height * 0.17, STARS); - drawStar(width * 0.66, height * 0.05, STARS); - drawStar(width * 0.13, height * 0.4, STARS); - drawStar(width * 0.72, height * 0.53, STARS); - drawStar(width * 0.81, height * 0.47, STARS); - drawStar(width * 0.92, height * 0.07, STARS); - - // Moon - const radius = width * 0.04; - const circle1 = [width * 0.5, height * 0.15]; - const circle2 = [circle1[0] - (radius / 3), circle1[1] - (radius / 3)]; - - ctx.fillStyle = "white"; - ctx.beginPath(); - ctx.arc(circle1[0], circle1[1], radius, 0, Math.PI * 2, true); - ctx.fill(); - ctx.globalCompositeOperation = 'destination-out'; - ctx.beginPath(); - ctx.arc(circle2[0], circle2[1], radius, 0, Math.PI * 2, true); - ctx.fill(); - ctx.closePath(); - - ctx.globalCompositeOperation = 'source-over'; - - const xIntersect = 0.1875; - const yIntersect = 0.79375; - const intersection = [xIntersect * width, yIntersect * height]; - const adj = width * (0.5 - xIntersect); - const opp = height * (0.55 - (1 - yIntersect)); - - const indicatorRadius = width * 0.05; - const indicatorX = (adj * percentage) + intersection[0]; - const indicatorY = intersection[1] - (opp * percentage); - - let indicatorGradient = anCtx.createRadialGradient( - indicatorX, indicatorY, indicatorRadius * 0.2, - indicatorX, indicatorY, indicatorRadius * 0.9) - indicatorGradient.addColorStop(0, "rgb(240, 150, 80)"); - indicatorGradient.addColorStop(0.4, "rgba(240, 150, 80, 0.3)"); - indicatorGradient.addColorStop(1, "transparent"); - - anCtx.beginPath(); - drawCircle(anCtx, indicatorX, indicatorY, indicatorRadius, undefined, indicatorGradient); - - let counter = 0; - let degrees = 0; - let rad = 0; - let chance; - const clamp = 50; - function animate() { - counter++; - if (counter % 2 === 0) { - degrees = (degrees + 1) % (180 - clamp); - degrees = Math.max(clamp, degrees); - if (degrees === 110) { - chance = Math.random(); - if (chance > 0.4) { - degrees = 70 - } - } - rad = degrees * Math.PI / 180; - anCtx.clearRect( - indicatorX - indicatorRadius, indicatorY - indicatorRadius, - indicatorX, indicatorY - ) - indicatorGradient = ctx.createRadialGradient( - indicatorX, indicatorY, indicatorRadius * 0.2, - indicatorX, indicatorY, indicatorRadius * Math.sin(rad)); - indicatorGradient.addColorStop(0, "rgb(240, 150, 80)"); - indicatorGradient.addColorStop(0.3, "rgba(240, 150, 80, 0.3)"); - indicatorGradient.addColorStop(1, "transparent"); - drawCircle(anCtx, indicatorX, indicatorY, indicatorRadius, undefined, indicatorGradient); - } - requestAnimationFrame(animate); - } - - animate(); -} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 7e6fe0dc..555b7178 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,36 +1,113 @@ import { sineIn } from "svelte/easing"; -type DateStyle = Intl.DateTimeFormatOptions['dateStyle'] +type DateStyle = Intl.DateTimeFormatOptions["dateStyle"]; -export function formatDate(date: string, dateStyle: DateStyle = 'medium', locales = "en-gb"): string { - const formatter = new Intl.DateTimeFormat(locales, { dateStyle }); - return formatter.format(new Date(date)); +export function formatDate(date: string, dateStyle: DateStyle = "medium", locales = "en-gb"): string { + const formatter = new Intl.DateTimeFormat(locales, { dateStyle }); + return formatter.format(new Date(date)); +} + +export function randBetween(min: number, max: number) { + return Math.random() * (max - min) + min; +} + +export function randIntBetween(min: number, max: number) { + return Math.floor(randBetween(min - 1, max)); +} + +export function randVariance(value: number, variance: number) { + const min = value * (1 - variance); + const max = value * (1 + variance); + return randBetween(min, max); } export function dayOfYear(date: Date) { - const diff = date.valueOf() - new Date(date. getFullYear(), 0, 0).valueOf(); - return Math.floor( diff / (1000 * 60 * 60 * 24)); + const diff = date.valueOf() - new Date(date.getFullYear(), 0, 0).valueOf(); + return Math.floor(diff / (1000 * 60 * 60 * 24)); } export function randomMinMax(min: number, max: number) { - return Math.floor(Math.random() * (max - min + 1) + min) + return Math.floor(Math.random() * (max - min + 1) + min); } export function randomDirection(): 1 | -1 { - return Math.random() >= 0.5 ? 1 : -1; + return Math.random() >= 0.5 ? 1 : -1; } -export function comparePinnedPosts (p1: Post, p2: Post) { - return parsePinned(p2.pinned) - parsePinned(p1.pinned); +export function comparePinnedPosts(p1: Post, p2: Post) { + return parsePinned(p2.pinned) - parsePinned(p1.pinned); } const parsePinned = (pinned: boolean | undefined) => Number(pinned ?? 0); - export const grow = (_node: HTMLElement) => { - return { - duration: 100, - easing: sineIn, - css: (t: number) => `transform: scaleX(${t}); transform-origin: left` - } -} \ No newline at end of file + return { + duration: 100, + easing: sineIn, + css: (t: number) => `transform: scaleX(${t}); transform-origin: left`, + }; +}; + +export const randRangeRGBString = ( + redRange: [number, number] | number = [0, 255], + greenRange: [number, number] | number = [0, 255], + blueRange: [number, number] | number = [0, 255], +) => { + const getValue = (range: [number, number] | number) => { + return Array.isArray(range) ? randBetween(range[0], range[1]) : range; + }; + + const red = getValue(redRange); + const green = getValue(greenRange); + const blue = getValue(blueRange); + return `rgb(${red}, ${green}, ${blue})`; +}; + +export function timeNoun(time: Date): string { + const hour = time.getHours(); + return hourNoun(hour); +} + +export function hourNoun(hour: number): string { + switch (hour) { + case 6: + return "dawn"; + case 7: + return "sunrise"; + case 8: + return "morning"; + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + return "day"; + case 19: + return "evening"; + case 20: + return "sunset"; + case 21: + return "dusk"; + case 22: + case 23: + case 24: + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + return "night"; + default: + return "day"; + } +} + +export function timeAsTwelvethFraction(time: Date): number { + return time.getHours() + (6 % 12); +} diff --git a/src/lib/wordSoup.ts b/src/lib/wordSoup.ts deleted file mode 100644 index 4269dd11..00000000 --- a/src/lib/wordSoup.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { LANG_LOGO_COLORS } from "./data/codeLangData"; -import { resolveCollisions, type Rect, resolveCollision } from "./mechanics/rect"; -import { Vector2D, randomVector } from "./mechanics/vector"; - -export const DEFAULT_GRAVITY = new Vector2D(0, 0.2); -const DEFAULT_DAMPING_FACTOR = 0.3; -const DEFAULT_MIN_MAGNITUDE = 4.8 -const DEFAULT_FONT_AREA = 1200; -const DEFAULT_MIN_FONT = 32; - -const COLORS = [ - "blue", - "red", - "green", - "purple", - "orange", - "magenta", - "lime" -] - -function getRects(wordBlocks: WordBlock[]): Rect[] { - return wordBlocks.map(block => block.getRect()); -} - -function getColor(technology: string) { - if (LANG_LOGO_COLORS[technology]) { - return LANG_LOGO_COLORS[technology]; - } else { - const randomIndex = Math.floor(Math.random() * COLORS.length); - return COLORS[randomIndex] - } - -} - -function getProportionalFontSize(fontArea: number | undefined, value: number, minFontSize?: number) { - const result = ((fontArea ?? DEFAULT_FONT_AREA) * value); - return Math.max(result, minFontSize ?? DEFAULT_MIN_FONT); -} - -export class WordSoup { - - wordBlocks: WordBlock[] = []; - ctx: CanvasRenderingContext2D; - canvas: HTMLCanvasElement; - counter: number = 0; - randomForces: boolean = false; - gravity: Vector2D = DEFAULT_GRAVITY; - dampingFactor: number = DEFAULT_DAMPING_FACTOR; - - constructor(canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, words: Record, fontArea?: number, minFontSize?: number) { - this.canvas = canvas; - this.ctx = context; - this.ctx.textAlign = "left"; - this.ctx.textBaseline = "top"; - - Object.keys(words).forEach(word => { - this.wordBlocks.push( - new WordBlock( - word, - getProportionalFontSize(fontArea, words[word], minFontSize), - getColor(word))); - }) - - let lastPlacedBlock: WordBlock; - const nextPosition = new Vector2D(10, 10); - const maxBlockHeight = Math.max(...this.wordBlocks.map(b => b.height)); - this.wordBlocks.forEach(block => { - if (lastPlacedBlock) { - nextPosition.x += lastPlacedBlock.width + (Math.random() * lastPlacedBlock.width); - if (nextPosition.x + block.width > canvas.width) { - nextPosition.x = 10 + (Math.random() * 10); - nextPosition.y += (maxBlockHeight * 2) + 10 + (Math.random() * maxBlockHeight) - } - } - - block.position.x = nextPosition.x; - block.position.y = nextPosition.y; - lastPlacedBlock = block; - block.draw(this.ctx); - }); - } - - setForces(value: Vector2D, randomForces: boolean = false, dampingFactor?: number) { - this.randomForces = randomForces; - this.setGravity(value); - if (dampingFactor) { - this.dampingFactor = dampingFactor; - } - } - - setGravity(value: Vector2D) { - this.gravity = value; - } - - animate = () => { - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - const rects = getRects(this.wordBlocks); - this.wordBlocks.forEach(block => { - const force = this.randomForces ? randomVector(1, 2) : this.gravity; - this.updateVelocity(block, rects, force); - block.draw(this.ctx); - }); - requestAnimationFrame(this.animate); - } - - updateVelocity(word: WordBlock, collisionRects: Rect[], force: Vector2D) { - word.velocity.add(force); - - const potentialPos = new Vector2D(word.position.x + word.velocity.x, - word.position.y + word.velocity.y); - - const potentialRect: Rect = { - x1: potentialPos.x, - x2: potentialPos.x + word.width, - y1: potentialPos.y, - y2: potentialPos.y + word.height - } - - const collision = resolveCollisions(potentialRect, word.getRect(), collisionRects, word.velocity); - - let rectangleCollision = false; - let boundaryCollision = false; - - if (collision) { - rectangleCollision = true; - word.velocity.add(collision.response); - } - - if (potentialPos.x < 0 || potentialPos.x >= this.canvas.width - word.width || collision && Math.abs(collision.response.x) > 0) { - word.velocity.x = word.velocity.x * -1; - boundaryCollision = true; - } - - if (potentialPos.y < 0 || potentialPos.y >= this.canvas.height - word.height || collision && Math.abs(collision.response.y) > 0) { - word.velocity.y = word.velocity.y * -1; - boundaryCollision = true; - } - - if (boundaryCollision) { - word.velocity.attenuate(this.dampingFactor, DEFAULT_MIN_MAGNITUDE); - } - - word.position.add(word.velocity); - - if (rectangleCollision && collision) { - const response = resolveCollision(word.getRect(), collisionRects[collision.index], word.velocity) - if (response) { - word.position.add(response); - } - } - - if (word.position.x < 0) { - word.position.x = 1; - } - - if(word.position.x >= this.canvas.width - word.width) { - word.position.x = this.canvas.width - word.width - 1; - } - - if (word.position.y < 0) { - word.position.y = 1; - } - if (word.position.y >= this.canvas.height - word.height) { - word.position.y = this.canvas.height - word.height - 1; - } - } -} - -class WordBlock { - height: number - width: number - color: string - word: string - charSize: number; - velocity: Vector2D; - position: Vector2D = new Vector2D(0, 0); - rotationMomentum: number = 0; - - constructor(word: string, charSize: number, color: string) { - this.height = charSize + 8; - this.width = (charSize * word.length * 0.6) + 16; - this.color = color - this.word = word; - this.charSize = charSize; - this.velocity = new Vector2D(0, 0); - } - - draw(ctx: CanvasRenderingContext2D) { - this.position.round(); - ctx.beginPath(); - // ctx.fillRect(this.position.x, this.position.y, this.width, this.height); - ctx.roundRect(this.position.x, this.position.y, this.width, this.height, 12); - ctx.fillStyle = this.color; - ctx.fill(); - ctx.closePath(); - - ctx.beginPath(); - ctx.font = `bold ${this.charSize}px 'JetBrainsMono'`; - ctx.fillStyle = "white"; - ctx.fillText(this.word, this.position.x + 4, this.position.y + 8, this.width); - ctx.fill(); - ctx.closePath(); - } - - setRandomPosition(bounds: Vector2D) { - this.position.x = Math.floor((Math.random() * (bounds.x - this.width))); - this.position.y = Math.floor((Math.random() * (bounds.y - this.height))); - } - - getRect(): Rect { - return { - x1: this.position.x, - x2: this.position.x + this.width, - y1: this.position.y, - y2: this.position.y + this.height, - } - } -} - diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index af75fac2..551eb7ba 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,16 +1,16 @@ @@ -40,30 +38,30 @@
-
-
- +
+
+
-
- +
+
-
+
-
+
-
- +
+
-
+
diff --git a/src/routes/Footer.svelte b/src/routes/Footer.svelte index f9b53929..b10dd122 100644 --- a/src/routes/Footer.svelte +++ b/src/routes/Footer.svelte @@ -1,45 +1,45 @@
-
-
    -
  • -
  • -
  • -
  • -
- -
+
+
    +
  • +
  • +
  • +
  • +
+ +
\ No newline at end of file + .copyright { + font-weight: lighter; + text-align: center; + } + diff --git a/src/routes/Header.svelte b/src/routes/Header.svelte index 93e08b82..8ff4085d 100644 --- a/src/routes/Header.svelte +++ b/src/routes/Header.svelte @@ -16,9 +16,9 @@
  • Projects
  • - +
  • CV
  • diff --git a/src/routes/[slug]/+page.svelte b/src/routes/[slug]/+page.svelte index 1a0c5be3..2686b565 100644 --- a/src/routes/[slug]/+page.svelte +++ b/src/routes/[slug]/+page.svelte @@ -1,63 +1,65 @@ - {data.meta.title} - - + {data.meta.title} + +
    - -
    -

    {data.meta.title}

    -

    Published {date}

    - {#if project} - - {/if} -
    + +
    +

    {data.meta.title}

    +

    Published {date}

    + {#if project} + + {/if} +
    -
    - {#if data.meta.projectId == "programming"} -
    - { - {#each data.meta.technologies as tech, i} - {tech}{#if i < (data.meta.technologies.length - 1)}, {/if} - {/each} - } -
    -

    - {data.meta.description} -

    - {/if} - -
    +
    + {#if data.meta.projectId == "programming"} +
    + { + {#each data.meta.technologies as tech, i} + {tech}{#if i < data.meta.technologies.length - 1}, {/if} + {/each} + } +
    +

    + {data.meta.description} +

    + {/if} + +
    \ No newline at end of file + .description { + font-style: italic; + padding: 0.5em 2em 2em; + margin: 0; + } + diff --git a/src/routes/projects/[projectId]/+page.server.ts b/src/routes/projects/[projectId]/+page.server.ts index 6eb99fee..1cc7097d 100644 --- a/src/routes/projects/[projectId]/+page.server.ts +++ b/src/routes/projects/[projectId]/+page.server.ts @@ -1,38 +1,39 @@ -import { error } from '@sveltejs/kit'; -import { getProjectData } from '../../../lib/data/projectData.js'; -import { parseLanguageObject } from '../../../lib/githubApi.js'; +import { error } from "@sveltejs/kit"; +import { getProjectData } from "../../../lib/data/projectData.js"; +import { parseLanguageObject } from "../../../lib/api/githubApi.js"; // import { GH_URL, GH_REPO_TOKEN } from "$env/static/private" // import { dev } from "$app/environment"; -import { STATIC_LANG_DATA } from '../../../lib/data/codeLangData.js'; -import type { PageServerLoad } from './$types.js'; +import { STATIC_LANG_DATA } from "../../../lib/data/codeLangData.js"; +import type { PageServerLoad } from "./$types.js"; export const prerender = false; export const load = (async ({ fetch, params }) => { + try { + const response = await fetch("/api/posts"); + let posts: Post[] = await response.json(); + let ghData; try { - const response = await fetch("/api/posts"); - let posts: Post[] = await response.json(); - let ghData; - try { - if (params.projectId === "programming") { - // if (dev) { - ghData = parseLanguageObject(STATIC_LANG_DATA); - // } else { - // ghData = await getGhLanguageData(`${GH_URL}/repos`, GH_REPO_TOKEN); - // } - } - } catch (e) { - console.error(e); - } - posts = posts.filter(p => p.projectId === params.projectId); - const project = getProjectData(params.projectId); - - return { - posts, - project, - ghData - } - } catch { - error(404, `Hmmm couldn't find ${params.projectId}`) + if (params.projectId === "programming") { + // if (dev) { + ghData = parseLanguageObject(STATIC_LANG_DATA); + // } else { + // ghData = await getGhLanguageData(`${GH_URL}/repos`, GH_REPO_TOKEN); + // } + } + } catch (e) { + console.error(e); } -}) satisfies PageServerLoad \ No newline at end of file + posts = posts.filter((p) => p.projectId === params.projectId); + const project = getProjectData(params.projectId); + + return { + posts, + project, + ghData, + }; + } catch { + error(404, `Hmmm couldn't find ${params.projectId}`); + } +}) satisfies PageServerLoad; + diff --git a/src/routes/projects/[projectId]/+page.svelte b/src/routes/projects/[projectId]/+page.svelte index 9e3ab14b..a8d6cd9a 100644 --- a/src/routes/projects/[projectId]/+page.svelte +++ b/src/routes/projects/[projectId]/+page.svelte @@ -1,70 +1,69 @@ -

    {data.project?.name}

    - + -
    +
    {#if path.split("/projects/")[1] === "miniatures"} - + {/if}
    - {data.project?.description ?? ""} + {data.project?.description ?? ""}
    {#if path.split("/projects/")[1] === "programming"} - {#if data.ghData} - - {/if} + {#if data.ghData} + + {/if} {/if}
    -
    - {#each sortedPosts as post} - - {/each} -
    +
    + {#each sortedPosts as post} + + {/each} +
    {#if path.split("/projects/")[1] === "dnd"} - + {/if}
    - {data.project?.bonus ?? ""} + {data.project?.bonus ?? ""}
    diff --git a/src/routes/weather/+page.server.ts b/src/routes/weather/+page.server.ts index 1e1f7abd..ee6b532a 100644 --- a/src/routes/weather/+page.server.ts +++ b/src/routes/weather/+page.server.ts @@ -1,41 +1,60 @@ -import { error } from '@sveltejs/kit'; -import type { PageServerLoad } from './$types'; +// import { error } from '@sveltejs/kit'; +import { error } from "@sveltejs/kit"; +import type { PageServerLoad } from "./$types"; export const prerender = false; /* eslint-disable */ -const openMeteoBaseUrl = "https://api.open-meteo.com/v1/forecast" +const openMeteoBaseUrl = "https://api.open-meteo.com/v1/forecast"; // default to edinburgh let location = { latitude: 55.953251, - longitude: -3.188267 -} + longitude: -3.188267, +}; const fetchWeather = async (latitude: number, longitude: number) => { - const request = `${openMeteoBaseUrl}?latitude=${latitude}&longitude=${longitude}¤t_weather=true` + const request = `${openMeteoBaseUrl}?latitude=${latitude}&longitude=${longitude}¤t_weather=true`; const result = await fetch(request); const data = await result.json(); - console.log(data); return data; -} +}; export const load = (async () => { - if (typeof window !== "undefined" && "geolocation" in window.navigator) { - window.navigator.geolocation.getCurrentPosition(position => { - location = { - latitude: position.coords.latitude, - longitude: position.coords.longitude - } - }); + if ("geolocation" in navigator) { + window.navigator.geolocation.getCurrentPosition( + (position) => { + location = { + latitude: position.coords.latitude, + longitude: position.coords.longitude, + }; + }, + (err) => { + console.log("error", err); + }, + ); } else { + console.log(typeof window); console.log("geolocation permissions blocked, getting weather from Edinburgh instead"); } try { + console.log(`using coords: ${location.latitude} ${location.longitude}`); const weather = await fetchWeather(location.latitude, location.longitude); - return weather.current_weather ?? undefined; + const currentWeather = weather.current_weather ?? undefined; + return { + latitude: weather.latitude, + longitude: weather.longitude, + ...currentWeather, + }; } catch { - error(404, "Wasn't able to get any weather data.\n\n It's probably raining"); + throw error(404, "Wasn't able to get any weather data.\n\n It's probably raining"); + return { + temperature: 16.3, + windspeed: 18.4, + winddirection: 78, + weathercode: 17, + is_day: 1, + time: Date.now(), + }; } }) satisfies PageServerLoad; - diff --git a/src/routes/weather/+page.svelte b/src/routes/weather/+page.svelte index 6cedffa6..5e20f98a 100644 --- a/src/routes/weather/+page.svelte +++ b/src/routes/weather/+page.svelte @@ -1,35 +1,68 @@ + let time = new Date(data.time); -
    - -
    + let elapsedTime: number; + let frameRate: number; -
    -
    Time:
    -
    {time.toLocaleTimeString()}
    -
    Temperature:
    -
    {data.temperature} °C
    -
    Weather:
    -
    {weather}
    -
    Wind speed:
    -
    {data.windspeed} km/h
    -
    Wind direction:
    -
    {windDirection}
    -
    + let showDiagnostics: boolean = true; + let setTime: number = time.getHours(); + $: updateTime(setTime); + + function updateTime(hour: number) { + const newTime = time; + newTime?.setHours(hour, 0, 0); + time = newTime; + } + + +
    + +
    + + +
    +
    + + +
    +
    - + +
    + +
    +
    +
    Latt/Long:
    +
    {`${data.latitude}, ${data.longitude}`}
    +
    Time:
    +
    {time.toLocaleTimeString()}
    +
    Temperature:
    +
    {data.temperature} °C
    +
    Weather:
    +
    {weather}
    +
    Wind speed:
    +
    {data.windspeed} km/h
    +
    Wind direction:
    +
    {windDirection}
    +
    + {#if showDiagnostics && frameRate} +
    +
    + fps: {frameRate?.toFixed(1)} +
    +
    + {/if}
    \ No newline at end of file + diff --git a/src/routes/weather/Compass.svelte b/src/routes/weather/Compass.svelte index da2d6539..32544467 100644 --- a/src/routes/weather/Compass.svelte +++ b/src/routes/weather/Compass.svelte @@ -80,4 +80,4 @@ transform: rotate(calc(var(--angle) - 4deg)); } } - \ No newline at end of file + diff --git a/src/routes/weather/DrawCharacter.ts b/src/routes/weather/DrawCharacter.ts deleted file mode 100644 index af22556e..00000000 --- a/src/routes/weather/DrawCharacter.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Artist } from "../../Artist"; - -const AMBER = "rgb(245, 167, 66)"; -// const DARK_ORANGE = "rgb(200, 100, 100)"; -const WHITE = "white"; -const BLACK = "black"; -// const GREY = "rgb(50, 50, 50)"; -const ORANGE_BROWN = "rgb(220, 150, 110)"; -const SHADOW_BROWN = "rgb(120, 80, 20)"; -const DARK_SHADOW_BROWN = "rgb(71, 50, 44)"; -const DARK_GREY = "rgb(50, 50, 50)"; -const PINK = "rgb(220, 100, 150)"; -const DARK_PINK = "rgb(150, 50, 100)"; - - -export function drawCharacter(context: CanvasRenderingContext2D, x: number, y: number) { - - const a = new Artist(context, x, y); - - const HEAD_POINTS: Points = [ - // top - [15, 0], - // ear - [15, -5], - [20, 0], - [10, 10], - // side - [5, 20], - [0, 10], - [-10, 40], - [-25, 50], - // chin - [-10, 10], - [-10, 5], - [-10, 0] - ] - - const HEAD_PATCH_POINTS: Points = [ - [10, 0], - [15, -3], - [20, 0], - [10, 35], - [-20, -15], - [-15, -8], - [-20, 0] - ] - - const RIGHT_EAR_POINTS: Points = [ - // top arc - [5, -6], - [4, -2], - [5, -3], - [15, -5], - // down right - [35, 18], - [2, 10], - [-5, 20], - // up left - [-15, 30], - [-5, 5], - [-3, 0], - [-10, -5], - [-5, -10], - [-5, -10], - [5, -20], - [-5, -20] - ] - - const LEFT_EAR_POINTS: Points = [ - // top arc - [-5, -10], - [-15, -5], - [-12, 9], - // down right - [-20, 3], - [-10, 5], - [-5, 5], - [15, 35], - // up left - [10, 20], - [10, 5], - [12, -20], - [0, -25], - [5, -10], - ] - - const NOSE: Points = [ - [5, 0], - [6, 5], - [2, 9], - [-2, 4], - [-4, 2], - [-3, 0], - [-3, -1] - ] - - const TONGUE: Points = [ - [10, 4], - [3, -2], - [-1, 10], - [-4, 5], - [-4, 3], - [-4, 0] - ] - - a.drawShape(HEAD_POINTS, true, 0, 0, 2, ORANGE_BROWN, ORANGE_BROWN); - - a.reset(0, 10); - context.beginPath(); - a.drawNextLine(0, 35); - a.endX = a.startX + 20; - a.endY = a.startY + 35; - context.bezierCurveTo( - a.startX, a.startY, - a.startX, a.startY + 15, - a.endX, a.endY); - context.lineCap = 'round'; - context.lineWidth = 4; - context.strokeStyle = SHADOW_BROWN; - context.stroke(); - context.closePath(); - a.reset(0, 45); - context.beginPath(); - a.endX = a.startX - 20; - a.endY = a.startY + 35; - context.bezierCurveTo( - a.startX, a.startY, - a.startX, a.startY + 15, - a.endX, a.endY); - context.lineCap = 'round'; - context.lineWidth = 4; - context.strokeStyle = SHADOW_BROWN; - context.stroke(); - context.closePath(); - - a.reset(0, 3); - a.drawShape(HEAD_PATCH_POINTS, true, 0, 3, 2, DARK_SHADOW_BROWN, DARK_SHADOW_BROWN); - a.reset(25, -2); - a.drawShape(RIGHT_EAR_POINTS, false, 0, 0, 2, SHADOW_BROWN, SHADOW_BROWN); - a.reset(-25, 0); - a.drawShape(LEFT_EAR_POINTS, false, 0, 0, 2, SHADOW_BROWN, SHADOW_BROWN); - a.reset(0, 80); - a.drawShape(NOSE, true, 0, 80, 2, DARK_GREY, DARK_GREY); - drawEyes(context, x, y); - a.reset(0, 108); - a.drawShape(TONGUE, true, 0, 108, 2, DARK_PINK, PINK); - a.reset(45, 80); - drawFaceLines(context, a.startX, a.startY); -} - -function drawFaceLines(context: CanvasRenderingContext2D, x: number, y: number) { - context.beginPath(); - let startX = x; - let startY = y; - let endX = startX - 5; - let endY = startY + 5; - context.bezierCurveTo( - startX, startY, - startX -3, startY -3, - endX, endY); - - startX = endX; - startY = endY; - endX = startX - 25; - endY = startY + 25; - context.bezierCurveTo( - startX, startY, - startX, startY + 10, - endX, endY); - - startX = endX; - startY = endY; - endX = startX - 14; - endY = startY - 5; - context.bezierCurveTo( - startX, startY, - startX - 10, startY + 2, - endX, endY); - - startX = endX; - startY = endY; - endX = startX - 14; - endY = startY + 5; - context.bezierCurveTo( - startX, startY, - startX - 4, startY + 7, - endX, endY); - - startX = endX; - startY = endY; - endX = startX - 25; - endY = startY - 25; - context.bezierCurveTo( - startX, startY, - startX - 25, startY - 15, - endX, endY); - - startX = endX; - startY = endY; - endX = startX - 5; - endY = startY - 5; - context.bezierCurveTo( - startX, startY, - startX -2, startY -7, - endX, endY); - - context.lineCap = 'round'; - context.lineWidth = 4; - context.strokeStyle = SHADOW_BROWN; - context.stroke(); - context.closePath(); -} - -function drawEyes(context: CanvasRenderingContext2D, x: number, y: number) { - const eyeSize = 17; - const eyePosX = 25; - const eyeTopPosY = 50; - const eyeBottomPosy = 34; - - // Right eye - context.beginPath(); - context.arc(x + eyePosX, y + eyeTopPosY, eyeSize, Math.PI * 1.2, Math.PI * 1.9); - context.fillStyle = DARK_GREY; - context.fill(); - context.closePath(); - context.beginPath(); - context.arc(x + eyePosX + 2, y + eyeBottomPosy, eyeSize, Math.PI * 2.2, Math.PI * 0.9); - context.fillStyle = DARK_GREY; - context.fill(); - - // Right pupil - context.beginPath(); - context.arc(x + eyePosX + 2, y + eyeTopPosY - 10, 7, Math.PI * 2, Math.PI * 1.1); - context.fillStyle = AMBER; - context.fill(); - - context.beginPath(); - context.arc(x + eyePosX + 2, y + eyeTopPosY - 8, 3, 0, Math.PI * 2); - context.fillStyle = BLACK; - context.fill(); - - context.beginPath(); - context.arc(x + eyePosX + 1, y + eyeTopPosY - 9, 1, 0, Math.PI * 2); - context.fillStyle = WHITE; - context.fill(); - - // Left eye - context.beginPath(); - context.arc(x - eyePosX, y + eyeTopPosY, eyeSize, Math.PI * 1.1, Math.PI * 1.8); - context.fillStyle = DARK_GREY; - context.fill(); - context.closePath(); - context.beginPath(); - context.arc(x - eyePosX - 2, y + eyeBottomPosy, eyeSize, Math.PI * 2.1, Math.PI * 0.8); - context.fillStyle = DARK_GREY; - context.fill(); - - // Left pupil - context.beginPath(); - context.arc(x - eyePosX - 2, y + eyeTopPosY - 10, 7, Math.PI * 1.9, Math.PI * 1); - context.fillStyle = AMBER; - context.fill(); - - context.beginPath(); - context.arc(x - eyePosX - 2, y + eyeTopPosY - 8, 3, 0, Math.PI * 2); - context.fillStyle = BLACK; - context.fill(); - - context.beginPath(); - context.arc(x - eyePosX - 3, y + eyeTopPosY - 9, 1, 0, Math.PI * 2); - context.fillStyle = WHITE; - context.fill(); -} diff --git a/src/routes/weather/Scene.svelte b/src/routes/weather/Scene.svelte index 63c9eeb0..17678373 100644 --- a/src/routes/weather/Scene.svelte +++ b/src/routes/weather/Scene.svelte @@ -1,114 +1,141 @@ - - - - +
    + + + +
    - + \ No newline at end of file + .canvas-container { + position: relative; + margin: var(--margin); + width: 100%; + height: 100%; + } + + canvas { + display: block; + width: calc(100% - 2 * (var(--margin))); + top: var(--margin); + left: var(--margin); + bottom: var(--margin); + position: absolute; + } + + .static-canvas { + z-index: 0; + } + + .animation-canvas { + z-index: 1; + } + + .night { + background: var(--night); + } + + .dawn { + background: linear-gradient(var(--night), var(--dawn-dusk)); + } + + .sunrise { + background: linear-gradient(var(--dawn-dusk), var(--sunrise-sunset)); + } + + .morning { + background: linear-gradient(var(--morning), var(--day)); + } + + .day { + background: var(--day); + } + + .evening { + background: linear-gradient(var(--day), var(--dawn-dusk)); + } + + .sunset { + background: linear-gradient(var(--dawn-dusk), var(--sunrise-sunset)); + } + + .dusk { + background: linear-gradient(var(--night), var(--dawn-dusk)); + } + diff --git a/src/routes/weather/WeatherSceneAnimator.ts b/src/routes/weather/WeatherSceneAnimator.ts new file mode 100644 index 00000000..8f2c74c5 --- /dev/null +++ b/src/routes/weather/WeatherSceneAnimator.ts @@ -0,0 +1,140 @@ +import { Animator } from "$lib/canvas/Animator"; +import { Cloud } from "$lib/canvas/weather/animate/Cloud"; +import { Rain } from "$lib/canvas/weather/animate/Rain"; +import { Snow } from "$lib/canvas/weather/animate/Snow"; +import { Thunder } from "$lib/canvas/weather/animate/Thunder"; +import { FrameRate } from "$lib/canvas/weather/animate/utils"; +import { randIntBetween } from "$lib/utils"; +import { Weather } from "../../lib/data/weatherData"; + +export class WeatherSceneController extends Animator { + height: number; + width: number; + animators: Animator[]; + windspeed: number; + currentWeather: Weather; + frameRate: FrameRate; + + constructor(context: CanvasRenderingContext2D, canvasWidth: number, canvasHeight: number, windspeed: number, weather: Weather) { + super(context); + this.animators = []; + this.height = canvasHeight; + this.width = canvasWidth; + this.windspeed = windspeed; + this.currentWeather = weather; + + this.frameRate = new FrameRate(); + this.setWeather(weather); + } + + animate(): void { + this.ctx.clearRect(0, 0, this.width, this.height); + this.animators.forEach((a) => a.animate()); + this.frameRate.calculateFrameRate(); + } + + clearAnimators() { + this.animators.splice(0, this.animators.length); + } + + setWindspeed(windspeed: number) { + this.windspeed = windspeed; + this.setWeather(this.currentWeather); + } + + setWeather(weather: Weather) { + this.currentWeather = weather; + this.clearAnimators(); + + switch (weather) { + case Weather.Clear: + this.setClear(); + break; + case Weather.Cloudy: + this.setCloudy(15, this.windspeed / 2); + break; + case Weather.Overcast: + this.setOvercast(); + break; + case Weather.Rain: + this.setCloudy(); + this.setRain(); + this.setCloudy(); + break; + case Weather.Drizzle: + this.setCloudy(); + this.setDrizzle(); + break; + case Weather.Snow: + this.setCloudy(); + this.setSnow(); + break; + case Weather.Thunder: + this.setRain(); + this.setCloudy(); + this.setThunder(); + break; + case Weather.Fog: + this.setFog(); + break; + default: + break; + } + } + + setThunder() { + const thunder = new Thunder(this.ctx, this.height, this.width, 0.5); + this.animators.push(thunder); + } + + setClear() { + return; + } + + setCloudy(count: number = 10, speed: number = this.windspeed) { + for (let i = 0; i < count; i++) { + const gray = randIntBetween(100, 170) + this.animators.push( + new Cloud( + this.ctx, + randIntBetween(0, this.width - 100), + randIntBetween(0, this.height / 4), + randIntBetween(20, 60), + randIntBetween(10, 30), + speed, + `rgb(${gray}, ${gray}, ${gray})`, + ), + ); + } + } + + setOvercast() { + this.setCloudy(30, this.windspeed / 10); + } + + setRain() { + const rainFg = new Rain(this.ctx, 20, 0.3, this.windspeed, 10); + const rainBg = new Rain(this.ctx, 10, 0.8, this.windspeed, 5); + + this.animators.push(rainFg); + // this.animators.push(rainBg); + } + + setDrizzle() { + const rain = new Rain(this.ctx, 10, 0.3, this.windspeed, 5); + + this.animators.push(rain); + } + + setSnow() { + const snowFg = new Snow(this.ctx, 4, 0.9, this.windspeed, 40); + const snowBg = new Snow(this.ctx, 2, 0.9, this.windspeed, 20); + + this.animators.push(snowFg); + this.animators.push(snowBg); + } + + setFog() { + return; + } +} diff --git a/src/routes/weather/weatherData.ts b/src/routes/weather/weatherData.ts deleted file mode 100644 index c3e27435..00000000 --- a/src/routes/weather/weatherData.ts +++ /dev/null @@ -1,56 +0,0 @@ -export enum Weather { - Clear = "Clear", - Cloudy = "Cloudy", - Overcast = "Overcast", - Fog = "Fog", - Drizzle = "Drizzle", - Rain = "Rain", - Snow = "Snow", - Thunder = "Thunder" -} - -export enum Direction { - N = "North", - NE = "North East", - E = "East", - SE = "South East", - S = "South", - SW = "South West", - W = "West", - NW = "North West" -} - -export function getWeatherFromCode(code: number): Weather { - switch(code) { - case 0: - return Weather.Clear - case 1 | 2: - return Weather.Cloudy - case 3: - return Weather.Overcast - case 45 | 48: - return Weather.Fog - case 51 | 53 | 55 | 56 | 57: - return Weather.Drizzle - case 61 | 63 | 65 | 66 | 67 | 80 | 81 | 82: - return Weather.Rain - case 71 | 73 | 75 | 77 | 85 | 86: - return Weather.Snow - case 95 | 96 | 99: - return Weather.Thunder - default: - return Weather.Cloudy - } -} - -export function getDirectionFromAngle(angle: number): Direction { - if (angle >= 337.5 || angle < 22.5) return Direction.N; - if (angle >= 22.5 && angle < 67.5) return Direction.NE; - if (angle >= 67.5 && angle < 112.5) return Direction.E; - if (angle >= 112.5 && angle < 157.5) return Direction.SE; - if (angle >= 157.5 && angle < 202.5) return Direction.S; - if (angle >= 202.5 && angle < 247.5) return Direction.SW; - if (angle >= 247.5 && angle < 292.5) return Direction.W; - if (angle >= 292.5 && angle < 337.5) return Direction.NW; - return Direction.N; -} \ No newline at end of file diff --git a/src/routes/weather/weatherScene.ts b/src/routes/weather/weatherScene.ts deleted file mode 100644 index bfa9c54a..00000000 --- a/src/routes/weather/weatherScene.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { drawCircle } from "$lib/canvasUtils"; - -export function drawOrbit( - context: CanvasRenderingContext2D, - time: Date, - orbitCenX: number, - orbitCenY: number, - orbitRadius: number, - orbitBodyRadius: number) { - - // ctx.beginPath(); - // ctx.arc( - // orbitCentreX, - // orbitCentreY, - // orbitRadius, - // Math.PI, Math.PI * 2, false); - // ctx.stroke(); - const hours = time.getHours(); - const moduloTime = hours + 6 % 24; - const orbitAngle = Math.PI - (((2 * Math.PI / 24) * moduloTime) % Math.PI) - (Math.PI / 2); - const orbitLenX = Math.sin(orbitAngle) * orbitRadius; - const orbitLenY = Math.cos(orbitAngle) * orbitRadius; - const orbitPosX = orbitCenX - orbitLenX; - const orbitPosY = orbitCenY - orbitLenY; - - // sun or moon - let radialGradient; - if (hours >= 6 && hours < 18) { - radialGradient = getSunRadialGradient(context, orbitPosX, orbitPosY, orbitBodyRadius); - } else { - radialGradient = getMoonRadialGradient(context, orbitPosX, orbitPosY, orbitBodyRadius); - } - context.fillStyle = radialGradient; - context.arc( - orbitPosX, - orbitPosY, - orbitBodyRadius, - 0, 2*Math.PI, false - ); - context.fill(); - - // context.fillRect(orbitPositionX, orbitPositionY, orbitBodyRadius * 2, orbitBodyRadius * 2); -} - -function getSunRadialGradient(context: CanvasRenderingContext2D, x: number, y: number, r: number) { - const radialGradient = context.createRadialGradient(x, y, r * 0.8, x, y, r); - radialGradient.addColorStop(0, 'rgba(255, 240, 210, 1)'); - radialGradient.addColorStop(1,'rgba(255, 255, 0, 0)'); - return radialGradient; -} - -function getMoonRadialGradient(context: CanvasRenderingContext2D, x: number, y: number, r: number) { - const radialGradient = context.createRadialGradient(x, y, (r * 0.5), x, y, r * 0.8); - radialGradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); - radialGradient.addColorStop(1,'rgba(50, 50, 50, 0)'); - return radialGradient; -} - -// https://github.com/PavlyukVadim/amadev/blob/master/RecursiveTree/script.js -export function drawTree(context: CanvasRenderingContext2D, - startX: number, - startY: number, - length: number, - angle: number, - depth: number, - branchWidth: number) { - - let newLength, newAngle; - const rand = Math.random; - const maxAngle = 2 * Math.PI / 6; - const maxBranch = 3; - const endX = startX + length * Math.cos(angle); - const endY = startY + length * Math.sin(angle); - - context.beginPath(); - context.moveTo(startX, startY); - context.lineCap = 'round'; - context.lineWidth = branchWidth; - context.lineTo(endX, endY); - - if (depth <= 2) { - context.strokeStyle = `rgb(30, ${(((rand() * 64) + 128) >> 0)}, 0)`; - } - else { - context.strokeStyle = `rgb(30, ${(((rand() * 64) + 64) >> 0)}, 20)`; - } - - context.stroke(); - const newDepth = depth - 1; - - if(!newDepth) { - return; - } - const subBranches = (rand() * (maxBranch - 1)) + 1; - branchWidth *= 0.7; - - for (let i = 0; i < subBranches; i++) { - newAngle = angle + rand() * maxAngle - maxAngle * 0.5; - newLength = length * (0.7 + rand() * 0.3); - drawTree(context, endX, endY, newLength, newAngle, newDepth, branchWidth); - } - -} - -// export function drawClouds(context: CanvasRenderingContext2D, direction: number, density: number) { - -// } - -export function drawCloud(context: CanvasRenderingContext2D, x: number, y: number, size: number) { - - const sizeVarFactor = 0.8; - const rowVarFactor = 0.9; - const sizeVariance = size * sizeVarFactor; - // get random size between 0.75 and 1.5 x provided size - const getBlobSize = () => Math.floor((Math.random() * sizeVariance) + sizeVariance); - const getRowLength = (middle: number) => Math.floor((Math.random() * middle * rowVarFactor) + (middle * rowVarFactor)) - const numberOfRows = Math.floor((Math.random() * 3) + 3); - - let rowLength = 4; - let newSize = size; - let newPosX; - let newPosY; - const cloudColour = "rgba(255, 255, 255, 0.8)"; - for (let cy = 0; cy <= numberOfRows * size; cy += size) { - rowLength = getRowLength(rowLength); - for (let cx = 0; cx <= rowLength * size; cx += size) { - const offset = (Math.random() * newSize * 2) + newSize; - newSize = getBlobSize(); - newPosX = cx + x + offset - newPosY = cy + y - drawCircle(context, newPosX, newPosY, newSize, undefined, cloudColour) - } - } -} \ No newline at end of file diff --git a/src/routes/weather/weatherTime.ts b/src/routes/weather/weatherTime.ts deleted file mode 100644 index ebde42fc..00000000 --- a/src/routes/weather/weatherTime.ts +++ /dev/null @@ -1,44 +0,0 @@ -export function timeNoun(time: Date): string { - const hour = time.getHours(); - switch(hour){ - case 6: - return "dawn"; - case 7: - return "sunrise"; - case 8: - return "morning"; - case 9: - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: - return "day"; - case 19: - return "evening"; - case 20: - return "sunset"; - case 21: - return "dusk"; - case 22: - case 23: - case 24: - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - return "night"; - default: - return "day" - } -} - -export function timeAsTwelvethFraction(time: Date): number { - return time.getHours() + 6 % 12; -} \ No newline at end of file diff --git a/src/store.ts b/src/store.ts index d6d2e249..c3314e97 100644 --- a/src/store.ts +++ b/src/store.ts @@ -7,35 +7,35 @@ const defaultDarkTheme = true; const defaultColourTheme = "gold"; function getDarkTheme(): boolean { - return getLocalStorageDarkTheme() ?? defaultDarkTheme; + return getLocalStorageDarkTheme() ?? defaultDarkTheme; } function getColourTheme(): string { - return getLocalStorageColourTheme() ?? defaultColourTheme; + return getLocalStorageColourTheme() ?? defaultColourTheme; } export const isUsingDarkTheme: Writable = writable(defaultDarkTheme); export const currentColourTheme: Writable = writable(defaultColourTheme); export function setUpThemes() { - isUsingDarkTheme.set(getDarkTheme()); - currentColourTheme.set(getColourTheme()); + isUsingDarkTheme.set(getDarkTheme()); + currentColourTheme.set(getColourTheme()); - isUsingDarkTheme.subscribe((value: boolean) => { - if (browser) { - setLocalStorageDarkTheme(value); - } - applyDarkTheme(value); - }); - currentColourTheme.subscribe((theme: string) => { - if (browser) { - setLocalStorageColourTheme(theme); - } - applyColourTheme(theme); - }); + isUsingDarkTheme.subscribe((value: boolean) => { + if (browser) { + setLocalStorageDarkTheme(value); + } + applyDarkTheme(value); + }); + currentColourTheme.subscribe((theme: string) => { + if (browser) { + setLocalStorageColourTheme(theme); + } + applyColourTheme(theme); + }); } export function resetThemes() { - isUsingDarkTheme.set(defaultDarkTheme); - currentColourTheme.set(defaultColourTheme); -} \ No newline at end of file + isUsingDarkTheme.set(defaultDarkTheme); + currentColourTheme.set(defaultColourTheme); +} diff --git a/tsconfig.json b/tsconfig.json index bca81921..4a0e59e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,22 +12,9 @@ "target": "esnext", "module": "esnext", "moduleResolution": "Node", - "types": [ - "node", - "jest" - ], + "types": ["node", "jest"] }, - "include": [ - ".eslintrc.cjs", - "svelte.config.js", - "vite.config.ts", - "src/**/*", - ".svelte-kit/ambient.d.ts" - ], - "exclude": [ - "node_modules", - ], - "plugins": [ - "babel-plugin-transform-import-meta" - ] + "include": [".eslintrc.cjs", "svelte.config.js", "vite.config.ts", "src/**/*", ".svelte-kit/ambient.d.ts"], + "exclude": ["node_modules"], + "plugins": ["babel-plugin-transform-import-meta"] }