Merge branch 'main' of git.mstar.dev:mstar/linstrom
All checks were successful
/ docker (push) Successful in 1m54s
All checks were successful
/ docker (push) Successful in 1m54s
This commit is contained in:
commit
a3b3b73dab
19 changed files with 760 additions and 39 deletions
Binary file not shown.
|
@ -14,7 +14,11 @@
|
|||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"jdenticon": "^3.3.0",
|
||||
"node-vibrant": "^4.0.3",
|
||||
"pinia": "^3.0.3",
|
||||
"sass": "^1.89.2",
|
||||
"sass-loader": "^16.0.5",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
|
@ -31,6 +35,7 @@
|
|||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-vue": "~10.2.0",
|
||||
"fibers": "^5.0.3",
|
||||
"jiti": "^2.4.2",
|
||||
"jsdom": "^26.1.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
|
|
|
@ -1,22 +1,11 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import Header from '@/components/global/Header.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" />
|
||||
|
||||
<div class="wrapper">
|
||||
<HelloWorld msg="You did it!" />
|
||||
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Header />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--text: #0d0f0b;
|
||||
--background: #f6f7f3;
|
||||
--primary: #4a5733;
|
||||
--secondary: #b5c898;
|
||||
--accent: #7c9b4b;
|
||||
}
|
||||
:root {
|
||||
--text: #0d0f0b;
|
||||
--background: #f6f7f3;
|
||||
--primary: #4a5733;
|
||||
--secondary: #b5c898;
|
||||
--accent: #7c9b4b;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #f2f4f0;
|
||||
--background: #0b0c08;
|
||||
--primary: #bfcca8;
|
||||
--secondary: #546737;
|
||||
--accent: #96b464;
|
||||
}
|
||||
:root {
|
||||
--text: #f2f4f0;
|
||||
--background: #0b0c08;
|
||||
--primary: #bfcca8;
|
||||
--secondary: #546737;
|
||||
--accent: #96b464;
|
||||
}
|
||||
}
|
||||
:root {
|
||||
--border: var(--text);
|
||||
}
|
||||
|
||||
svg.black-white {
|
||||
fill: var(--text);
|
||||
stroke: var(--text);
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=base.css.map */
|
||||
|
|
1
frontend-vue/src/assets/base.css.map
Normal file
1
frontend-vue/src/assets/base.css.map
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"sourceRoot":"","sources":["base.scss"],"names":[],"mappings":"AAGA;EACC;IACC;IACA;IACA;IACA;IACA;;;AAIF;EACC;IACC;IACA;IACA;IACA;IACA;;;AAKF;EACE;;;AAGF;EACI;EACA","file":"base.css"}
|
32
frontend-vue/src/assets/base.scss
Normal file
32
frontend-vue/src/assets/base.scss
Normal file
|
@ -0,0 +1,32 @@
|
|||
@use "sass:color";
|
||||
|
||||
// Base colors here
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--text: #0d0f0b;
|
||||
--background: #f6f7f3;
|
||||
--primary: #4a5733;
|
||||
--secondary: #b5c898;
|
||||
--accent: #7c9b4b;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #f2f4f0;
|
||||
--background: #0b0c08;
|
||||
--primary: #bfcca8;
|
||||
--secondary: #546737;
|
||||
--accent: #96b464;
|
||||
}
|
||||
}
|
||||
|
||||
// And calculate the rest here
|
||||
:root {
|
||||
--border: var(--text);
|
||||
}
|
||||
|
||||
svg.black-white {
|
||||
fill: var(--text);
|
||||
stroke: var(--text);
|
||||
}
|
|
@ -1,4 +1,12 @@
|
|||
@import './base.css';
|
||||
@import 'base.scss';
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
}
|
||||
|
||||
p, h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
|
@ -10,7 +18,7 @@
|
|||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
color: var(--accent);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
margin-left: 2rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
i {
|
||||
|
@ -31,28 +32,28 @@ i {
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
padding: 0.4rem 0 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border: 1px solid var(--text);
|
||||
background: var(--background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
@ -60,7 +61,7 @@ h3 {
|
|||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-left: 1px solid var(--text);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
|
@ -69,7 +70,7 @@ h3 {
|
|||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
border-left: 1px solid var(--text);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
|
|
40
frontend-vue/src/components/global/Header.vue
Normal file
40
frontend-vue/src/components/global/Header.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<script setup lang="ts">
|
||||
|
||||
import { RouterLink } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<div class="links">
|
||||
<nav>
|
||||
<RouterLink to="/">Home</RouterLink>
|
||||
<RouterLink to="/about">About</RouterLink>
|
||||
<RouterLink to="/testing/note">Test note</RouterLink>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.links {
|
||||
border-radius: 1em;
|
||||
border: 0.2em solid var(--border);
|
||||
}
|
||||
nav a.router-link-exact-active {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
nav a.router-link-exact-active:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
nav a {
|
||||
display: inline-block;
|
||||
padding: 0 1rem;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
nav a:first-of-type {
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
30
frontend-vue/src/components/user/PreviewHeader.vue
Normal file
30
frontend-vue/src/components/user/PreviewHeader.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import { defineProps } from "vue"
|
||||
import type { User } from '../../stores/userdata'
|
||||
import ProfilePicture from '@/components/user/ProfilePicture.vue'
|
||||
import FormattedText from '@/components/util/FormattedText.vue'
|
||||
const props = defineProps<{
|
||||
user: User
|
||||
}>()
|
||||
</script>
|
||||
<template>
|
||||
<div class="account-header">
|
||||
<ProfilePicture :username="props.user.username" :media="props.user.profilePicture" />
|
||||
<div class="account-info">
|
||||
<FormattedText :raw-text="props.user.displayName"/>
|
||||
<p>{{props.user.username}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.account-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.account-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
57
frontend-vue/src/components/user/ProfilePicture.vue
Normal file
57
frontend-vue/src/components/user/ProfilePicture.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<script setup lang="ts">
|
||||
import { toSvg} from 'jdenticon'
|
||||
import type { MediaMetadata } from '@/stores/media.ts'
|
||||
import { Vibrant } from 'node-vibrant/node'
|
||||
import { useTemplateRef } from 'vue'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
const props = defineProps<{
|
||||
media?: MediaMetadata
|
||||
username: string
|
||||
}>()
|
||||
|
||||
/* async function calcBackgroundColor() {
|
||||
const imgWrapper = useTemplateRef<HTMLImageElement>("profilePicture");
|
||||
if (!imgWrapper) return;
|
||||
const img = imgWrapper.value;
|
||||
if (!img) return null;
|
||||
const vibrant = Vibrant.from(img).build();
|
||||
const palette = await vibrant.getPalette();
|
||||
if (!palette.Vibrant) return;
|
||||
const r = (255 - palette.Vibrant.r).toString(16),
|
||||
g = (255 - palette.Vibrant.g).toString(16),
|
||||
b = (255 - palette.Vibrant.b).toString(16);
|
||||
// pad each with zeros and return
|
||||
return '#' + padZero(r) + padZero(g) + padZero(b);
|
||||
}
|
||||
|
||||
function padZero(str: string, len?: number): string {
|
||||
len = len || 2;
|
||||
const zeros = new Array(len).join('0');
|
||||
return (zeros + str).slice(-len);
|
||||
}
|
||||
|
||||
async function calcBackgroundStyle() {
|
||||
const color = await calcBackgroundColor();
|
||||
if (!color) return "";
|
||||
return "background-color: "+color;
|
||||
}
|
||||
*/
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.media" aria-label="profile picture">
|
||||
<!-- TODO: Inverted primary color of image
|
||||
Get primary color of the image, invert it and set as background for higher contrast in case of transparency
|
||||
calcBackgroundStyle *should* do it, but async and vue no like
|
||||
-->
|
||||
<img :src="props.media.url" :alt="props.media.alt" height="52" width="52" ref="profilePicture"/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
aria-label="profile picture"
|
||||
aria-description="No profile picture set, this is a generated identicon"
|
||||
>
|
||||
<div v-html="toSvg(props.username, 128)" aria-hidden="true"/>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { defineProps } from "vue"
|
||||
const props = defineProps<{
|
||||
rawText: string // Raw text (already pre-transformed by server into Linstrom format)
|
||||
}>()
|
||||
|
@ -7,4 +6,6 @@ import { defineProps } from "vue"
|
|||
// Probably something akin to the treeificator. Marshal text into tree first,
|
||||
// then adjust tree nodes to types that unmarshal into html
|
||||
</script>
|
||||
<p>{{props.rawText}}</p>
|
||||
<template>
|
||||
<p>{{props.rawText}}</p>
|
||||
</template>
|
||||
|
|
|
@ -17,6 +17,11 @@ const router = createRouter({
|
|||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/testing/note',
|
||||
name: 'note-testing',
|
||||
component: () => import('../views/NoteTestView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
|
10
frontend-vue/src/stores/media.ts
Normal file
10
frontend-vue/src/stores/media.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export interface MediaMetadata {
|
||||
id: string;
|
||||
ownedBy: string;
|
||||
remote: boolean;
|
||||
url: string;
|
||||
mediaType: string;
|
||||
name: string;
|
||||
alt: string;
|
||||
blurred: boolean;
|
||||
}
|
10
frontend-vue/src/stores/userdata.ts
Normal file
10
frontend-vue/src/stores/userdata.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import type { MediaMetadata } from '@/stores/media.ts'
|
||||
|
||||
export interface User {
|
||||
username: string
|
||||
displayName: string
|
||||
description: string
|
||||
profilePicture?: MediaMetadata
|
||||
bannerPicture?: MediaMetadata
|
||||
backgroundPicture?: MediaMetadata
|
||||
}
|
492
frontend-vue/src/utils/identiheart.ts
Normal file
492
frontend-vue/src/utils/identiheart.ts
Normal file
|
@ -0,0 +1,492 @@
|
|||
class Crusher {
|
||||
hash(s: any): number {
|
||||
return String(s).split("").reduce(function(a, b) {
|
||||
a = ((a << 5) - a) + b.charCodeAt(0);
|
||||
return a & a
|
||||
}, 0);
|
||||
}
|
||||
isDOMElement(o:any): boolean {
|
||||
return (
|
||||
typeof HTMLElement === "object" ? o instanceof HTMLElement :
|
||||
o && typeof o === "object" && true && o.nodeType === 1 && typeof o.nodeName==="string"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum BlockType {
|
||||
ONE = 1,
|
||||
TWO = 2,
|
||||
THREE = 3,
|
||||
}
|
||||
|
||||
class Block {
|
||||
canvas: HTMLCanvasElement;
|
||||
context: CanvasRenderingContext2D;
|
||||
type: BlockType;
|
||||
primary: string;
|
||||
accent: string;
|
||||
cellSize: number;
|
||||
margin: number;
|
||||
scale: number;
|
||||
pos: Position;
|
||||
hash: number;
|
||||
|
||||
constructor(
|
||||
c: HTMLCanvasElement,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
type: BlockType,
|
||||
primary: string,
|
||||
accent: string,
|
||||
hash: number,
|
||||
cellSize: number,
|
||||
margin: number,
|
||||
scale: number,
|
||||
pos: Position,
|
||||
) {
|
||||
this.canvas = c;
|
||||
this.context = ctx;
|
||||
this.type = type;
|
||||
this.primary = primary;
|
||||
this.accent = accent;
|
||||
this.hash = hash;
|
||||
this.cellSize = cellSize;
|
||||
this.margin = margin;
|
||||
this.scale = scale;
|
||||
this.pos = pos;
|
||||
}
|
||||
|
||||
offset() {
|
||||
this.context.save();
|
||||
this.context.translate(0.6 * this.scale, -0.6 * this.scale);
|
||||
}
|
||||
|
||||
resetOffset() {
|
||||
this.context.restore();
|
||||
}
|
||||
|
||||
makePath(hash: number, offset: number) {
|
||||
const mod = Math.abs(hash + offset) % 4;
|
||||
|
||||
switch(mod) {
|
||||
case 0:
|
||||
// top
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + this.cellSize, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + this.cellSize, this.pos.y + this.cellSize);
|
||||
this.context.closePath();
|
||||
break;
|
||||
case 1:
|
||||
// right
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x + this.cellSize, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + this.cellSize, this.pos.y + this.cellSize);
|
||||
this.context.lineTo(this.pos.x, this.pos.y + this.cellSize);
|
||||
this.context.closePath();
|
||||
break;
|
||||
case 2:
|
||||
// bottom
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x, this.pos.y);
|
||||
this.context.lineTo(this.pos.x, this.pos.y + this.cellSize);
|
||||
this.context.lineTo(this.pos.x + this.cellSize, this.pos.y + this.cellSize);
|
||||
this.context.closePath();
|
||||
break;
|
||||
case 3:
|
||||
// left
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + this.cellSize, this.pos.y);
|
||||
this.context.lineTo(this.pos.x, this.pos.y + this.cellSize);
|
||||
this.context.closePath();
|
||||
break;
|
||||
default:
|
||||
// top
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + this.cellSize, this.pos.y);
|
||||
this.context.lineTo(this.pos.x, this.pos.y + this.cellSize);
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.offset();
|
||||
|
||||
if (this.type === BlockType.ONE) {
|
||||
this.makePath(this.hash, this.hash % 3);
|
||||
this.context.fillStyle = this.primary;
|
||||
this.context.fill();
|
||||
|
||||
this.makePath(this.hash, this.hash % 5);
|
||||
this.context.fillStyle = this.accent;
|
||||
this.context.fill();
|
||||
} else if (this.type === BlockType.TWO) {
|
||||
this.makePath(this.hash, this.hash % 4);
|
||||
this.context.fillStyle = this.accent;
|
||||
this.context.fill();
|
||||
|
||||
this.makePath(this.hash, this.hash % 3);
|
||||
this.context.fillStyle = this.primary;
|
||||
this.context.fill();
|
||||
} else {
|
||||
this.makePath(this.hash, this.hash % 7);
|
||||
this.context.fillStyle = this.accent;
|
||||
this.context.fill();
|
||||
|
||||
this.makePath(this.hash, this.hash % 8);
|
||||
this.context.fillStyle = this.primary;
|
||||
this.context.fill();
|
||||
}
|
||||
|
||||
this.resetOffset();
|
||||
}
|
||||
}
|
||||
|
||||
class Position {
|
||||
x: number;
|
||||
y: number;
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
|
||||
class Shape {
|
||||
canvas: HTMLCanvasElement;
|
||||
context: CanvasRenderingContext2D;
|
||||
hash: number;
|
||||
primary: string;
|
||||
accent: string;
|
||||
pos: Position;
|
||||
scale: number;
|
||||
cellSize: number;
|
||||
strokeColor: string;
|
||||
|
||||
constructor(
|
||||
c: HTMLCanvasElement,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
hash: number,
|
||||
primary: string,
|
||||
accent: string,
|
||||
pos: Position,
|
||||
scale: number,
|
||||
cellSize: number,
|
||||
strokeColor: string
|
||||
) {
|
||||
this.canvas = c;
|
||||
this.context = ctx;
|
||||
this.hash = hash;
|
||||
this.primary = primary;
|
||||
this.accent = accent;
|
||||
this.pos = pos;
|
||||
this.scale = scale;
|
||||
this.cellSize = cellSize;
|
||||
this.strokeColor = strokeColor;
|
||||
}
|
||||
|
||||
getColor() {
|
||||
return [this.primary, this.accent][Math.abs(this.hash % 2)];
|
||||
}
|
||||
|
||||
makePath() {
|
||||
const mod = Math.abs(this.hash + 1) % 4;
|
||||
|
||||
switch(mod) {
|
||||
case 0:
|
||||
// square
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y);
|
||||
this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y - (this.cellSize / 2));
|
||||
this.context.lineTo(this.pos.x, this.pos.y - (this.cellSize / 2));
|
||||
this.context.closePath();
|
||||
break;
|
||||
case 1:
|
||||
//circle
|
||||
this.context.beginPath();
|
||||
this.context.arc(
|
||||
this.pos.x + (this.cellSize / Math.PI) - 5,
|
||||
this.pos.y - (this.cellSize / Math.PI) + 5,
|
||||
this.cellSize / 3,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
true
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
// triangle
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + (this.cellSize * 0.65), this.pos.y);
|
||||
this.context.lineTo(this.pos.x, this.pos.y - (this.cellSize * 0.65));
|
||||
this.context.closePath();
|
||||
break;
|
||||
case 3:
|
||||
// oval
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x - (this.cellSize * 0.2), this.pos.y + (this.cellSize * 0.2));
|
||||
this.context.quadraticCurveTo(this.pos.x + (this.cellSize * 0.4), this.pos.y, this.pos.x + (this.cellSize * 0.5), this.pos.y - (this.cellSize * 0.5));
|
||||
this.context.moveTo(this.pos.x + (this.cellSize * 0.5), this.pos.y - (this.cellSize * 0.5));
|
||||
this.context.quadraticCurveTo(this.pos.x , this.pos.y - (this.cellSize * 0.4), this.pos.x - (this.cellSize * 0.2), this.pos.y + (this.cellSize * 0.2));
|
||||
break;
|
||||
default:
|
||||
//square
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.pos.x, this.pos.y);
|
||||
this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y);
|
||||
this.context.lineTo(this.pos.x + (this.cellSize / 2), this.pos.y - (this.cellSize / 2));
|
||||
this.context.lineTo(this.pos.x, this.pos.y - (this.cellSize / 2));
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
draw(hasStroke: boolean, strokeWeight: number) {
|
||||
const color = this.getColor();
|
||||
this.context.globalCompositeOperation = "source-over";
|
||||
|
||||
this.makePath();
|
||||
this.context.fillStyle = color;
|
||||
this.context.strokeStyle = this.strokeColor;
|
||||
this.context.lineWidth = this.scale * ((4/5 * strokeWeight) / this.canvas.width);
|
||||
this.context.lineJoin = "round";
|
||||
this.context.lineCap = "round";
|
||||
this.context.fill();
|
||||
|
||||
if (hasStroke) {
|
||||
this.context.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @name IdentiHeart
|
||||
* @author Schlipak
|
||||
* @copyright Apache license 2015 Guillaume de Matos
|
||||
*/
|
||||
export class Identiheart {
|
||||
canvas: HTMLCanvasElement
|
||||
context: CanvasRenderingContext2D
|
||||
margin: number
|
||||
crusher: Crusher
|
||||
palette = [
|
||||
'#F44336', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3',
|
||||
'#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39',
|
||||
'#FFEB3B', '#FFC107', '#FF9800', '#FF5722', '#795548', '#607D8B'
|
||||
];
|
||||
|
||||
primary: string = "#F44336";
|
||||
accent: string = "#E91E63";
|
||||
scale: number;
|
||||
cellSize: number;
|
||||
hash: number = 0;
|
||||
blocks: Block[] = [];
|
||||
shape?: Shape;
|
||||
hasStroke: boolean = true;
|
||||
strokeWeight = 500;
|
||||
strokeColor = "#000000";
|
||||
compositeOperation: GlobalCompositeOperation = 'multiply';
|
||||
|
||||
constructor(c: HTMLCanvasElement, margin?: number, scale?: number, ctx?: CanvasRenderingContext2D) {
|
||||
this.canvas = c;
|
||||
this.context = ctx ?? c.getContext("2d")!;
|
||||
this.margin = margin || 5;
|
||||
this.scale = scale || 20;
|
||||
this.crusher = new Crusher();
|
||||
this.cellSize = (this.canvas.width / 2) - (this.margin * this.scale);
|
||||
}
|
||||
|
||||
setUsername(username: string) {
|
||||
this.hash = this.crusher.hash(username);
|
||||
return this;
|
||||
}
|
||||
|
||||
setPalette(palette:string[]) {
|
||||
if (palette.length < 2) {
|
||||
throw new Error('Palette must be more than two elements long');
|
||||
}
|
||||
|
||||
this.palette = palette;
|
||||
return this;
|
||||
}
|
||||
|
||||
setHasStroke(b: boolean) {
|
||||
this.hasStroke = b;
|
||||
return this;
|
||||
}
|
||||
|
||||
setStrokeWeight(weight: number) {
|
||||
this.strokeWeight = weight;
|
||||
return this;
|
||||
}
|
||||
|
||||
setStrokeColor(color: string) {
|
||||
this.strokeColor = color;
|
||||
return this;
|
||||
}
|
||||
|
||||
setCompositeOperation(operation: GlobalCompositeOperation) {
|
||||
this.compositeOperation = operation;
|
||||
return this;
|
||||
}
|
||||
|
||||
setCanvas(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this.context = canvas.getContext('2d')!;
|
||||
return this;
|
||||
}
|
||||
|
||||
init() {
|
||||
// Purge the block array
|
||||
this.blocks = new Array<Block>();
|
||||
// Purge the shape
|
||||
this.shape = undefined;
|
||||
|
||||
// Generate colors
|
||||
const crusher = new Crusher();
|
||||
const subHash = crusher.hash(this.hash);
|
||||
this.accent = this.palette[Math.abs(subHash % this.palette.length)];
|
||||
|
||||
// Clear the canvas
|
||||
this.context.globalCompositeOperation = "source-over";
|
||||
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.context.globalCompositeOperation = this.compositeOperation;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
draw() {
|
||||
this.init();
|
||||
|
||||
// Rotate the canvas -45deg
|
||||
this.context.save();
|
||||
this.context.translate(this.canvas.width/2, this.canvas.height/2);
|
||||
this.context.rotate(- Math.PI / 4);
|
||||
this.context.translate(-this.canvas.width/2, -this.canvas.height/2);
|
||||
|
||||
this.generateBlocks();
|
||||
this.drawBlocks();
|
||||
|
||||
if (this.hasStroke) {
|
||||
this.drawOutline();
|
||||
}
|
||||
|
||||
this.shape = new Shape(this.canvas, this.context, this.hash, this.primary, this.accent, {
|
||||
x: (this.margin * this.scale) + 1.5 * this.cellSize,
|
||||
y: (this.margin * this.scale) + 0.5 * this.cellSize
|
||||
}, this.scale, this.cellSize, this.strokeColor);
|
||||
this.shape.draw(this.hasStroke, this.strokeWeight);
|
||||
|
||||
// Restore the original matrix
|
||||
this.context.restore();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
offset() {
|
||||
this.context.save();
|
||||
this.context.translate(0.6 * this.scale, - 0.6 * this.scale);
|
||||
}
|
||||
|
||||
resetOffset() {
|
||||
this.context.restore();
|
||||
}
|
||||
|
||||
drawOutline() {
|
||||
this.offset();
|
||||
this.context.globalCompositeOperation = "source-over";
|
||||
|
||||
// Outer lines
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.margin * this.scale, this.margin * this.scale);
|
||||
this.context.lineTo(this.margin * this.scale, this.canvas.height - (this.margin * this.scale));
|
||||
this.context.lineTo(this.canvas.width - (this.margin * this.scale), this.canvas.height - (this.margin * this.scale));
|
||||
this.context.lineTo(this.canvas.width - (this.margin * this.scale), this.canvas.height / 2);
|
||||
this.context.lineTo(this.canvas.width / 2, this.canvas.height / 2);
|
||||
this.context.lineTo(this.canvas.width / 2, this.margin * this.scale);
|
||||
this.context.closePath();
|
||||
|
||||
this.context.strokeStyle = this.strokeColor;
|
||||
this.context.lineWidth = this.scale * (this.strokeWeight / this.canvas.width);
|
||||
this.context.lineJoin = "round";
|
||||
this.context.lineCap = "round";
|
||||
this.context.stroke();
|
||||
|
||||
// Inner lines
|
||||
this.context.beginPath();
|
||||
this.context.moveTo(this.canvas.width / 2, this.canvas.height / 2);
|
||||
this.context.lineTo(this.margin * this.scale, this.canvas.height / 2);
|
||||
this.context.moveTo(this.canvas.width / 2, this.canvas.height / 2);
|
||||
this.context.lineTo(this.canvas.width / 2, this.canvas.height - (this.margin * this.scale));
|
||||
|
||||
this.context.stroke();
|
||||
|
||||
this.resetOffset();
|
||||
this.context.globalCompositeOperation = this.compositeOperation;
|
||||
}
|
||||
|
||||
generateBlocks() {
|
||||
/*
|
||||
c: HTMLCanvasElement,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
type: BlockType,
|
||||
primary: string,
|
||||
accent: string,
|
||||
hash: number,
|
||||
cellSize: number,
|
||||
margin: number,
|
||||
scale: number,
|
||||
pos: Position,
|
||||
* */
|
||||
let b1 = new Block(
|
||||
this.canvas,
|
||||
this.context,
|
||||
BlockType.ONE,
|
||||
this.primary,
|
||||
this.accent,
|
||||
this.hash,
|
||||
this.cellSize,
|
||||
this.margin,
|
||||
this.scale,
|
||||
new Position(this.margin * this.scale,this.margin * this.scale));
|
||||
this.blocks.push(b1);
|
||||
|
||||
let b2 = new Block(
|
||||
this.canvas,
|
||||
this.context,
|
||||
BlockType.TWO,
|
||||
this.primary, this.accent,
|
||||
this.hash,
|
||||
this.cellSize,
|
||||
this.margin,
|
||||
this.scale,
|
||||
new Position(this.margin * this.scale,this.canvas.height / 2)
|
||||
);
|
||||
this.blocks.push(b2);
|
||||
|
||||
let b3 = new Block(
|
||||
this.canvas,
|
||||
this.context,
|
||||
BlockType.THREE,
|
||||
this.primary,
|
||||
this.accent,
|
||||
this.hash,
|
||||
this.cellSize,
|
||||
this.margin,
|
||||
this.scale,
|
||||
new Position(this.canvas.width / 2, this.canvas.height / 2)
|
||||
);
|
||||
this.blocks.push(b3);
|
||||
}
|
||||
|
||||
drawBlocks() {
|
||||
if (this.blocks.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.blocks.length; i++) {
|
||||
this.blocks[i].draw();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@
|
|||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
29
frontend-vue/src/views/NoteTestView.vue
Normal file
29
frontend-vue/src/views/NoteTestView.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import type { MediaMetadata } from '@/stores/media.ts'
|
||||
import type { User } from '@/stores/userdata.ts'
|
||||
import PreviewHeader from '@/components/user/PreviewHeader.vue'
|
||||
|
||||
const sampleMedia = {
|
||||
url: "https://valco.fi/cdn/shop/files/nettisivu_valkoinen.webp",
|
||||
alt: "valko logo",
|
||||
blurred: false,
|
||||
mediaType: "image/webp",
|
||||
name: "valko-logo.webp",
|
||||
ownedBy: "some-user-id",
|
||||
remote: false,
|
||||
id: "some-media-api"
|
||||
} as MediaMetadata;
|
||||
const user = {
|
||||
username: "valko",
|
||||
displayName: "valko",
|
||||
profilePicture: sampleMedia
|
||||
} as User;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PreviewHeader :user="user" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue