More Poz (#57)
* update profile pictures * small clean up * more poz on clicks * update profiles * edgepoz * clean up * add poz eye image
|
@ -21,10 +21,11 @@
|
||||||
"image": "https://cdn.discordapp.com/avatars/378704069726044170/415dcb2ef8d1ef635e35e1d04d523cba.webp",
|
"image": "https://cdn.discordapp.com/avatars/378704069726044170/415dcb2ef8d1ef635e35e1d04d523cba.webp",
|
||||||
"class": "outline-amber-500",
|
"class": "outline-amber-500",
|
||||||
"coordinates": [568, 594],
|
"coordinates": [568, 594],
|
||||||
"size": 120
|
"size": 120,
|
||||||
|
"tag": "le_mod"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "https://cdn.discordapp.com/avatars/623781003382751243/95435efc86709ac7e347ca205bda665e.webp",
|
"image": "https://cdn.discordapp.com/avatars/623781003382751243/0705cf015336ae06cefcdcb2800a49e9.webp",
|
||||||
"class": "outline-orange-500",
|
"class": "outline-orange-500",
|
||||||
"coordinates": [525, 764],
|
"coordinates": [525, 764],
|
||||||
"size": 80
|
"size": 80
|
||||||
|
@ -120,13 +121,6 @@
|
||||||
"coordinates": [263, 653],
|
"coordinates": [263, 653],
|
||||||
"size": 65
|
"size": 65
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"image": "https://cdn.discordapp.com/avatars/273110996938260481/5d2ea7a0ad5a29e0de5b3918bfa82ab2.webp",
|
|
||||||
"class": "outline-yellow-500 bg-black ",
|
|
||||||
"coordinates": [893, 622],
|
|
||||||
"size": 80,
|
|
||||||
"quote": "\"piss blob\""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"image": "https://cdn.discordapp.com/avatars/231040215085481984/7f378337240110b76e6e9baa31f83670.webp",
|
"image": "https://cdn.discordapp.com/avatars/231040215085481984/7f378337240110b76e6e9baa31f83670.webp",
|
||||||
"class": "outline-amber-500",
|
"class": "outline-amber-500",
|
||||||
|
@ -159,7 +153,7 @@
|
||||||
"size": 35
|
"size": 35
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "https://cdn.discordapp.com/avatars/390226958241366016/513b77487a9fa01b1256c4dc8e1b0c55.webp",
|
"image": "https://cdn.discordapp.com/avatars/390226958241366016/4c149bb6d2ba4ed1265a36c2c9c4418a.webp",
|
||||||
"class": "outline-blue-500 ",
|
"class": "outline-blue-500 ",
|
||||||
"coordinates": [858, 707],
|
"coordinates": [858, 707],
|
||||||
"size": 45
|
"size": 45
|
||||||
|
@ -177,7 +171,7 @@
|
||||||
"size": 47
|
"size": 47
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "https://cdn.discordapp.com/avatars/194584980922433536/83b3e21452d96c9a3443717dbcddb4bf.webp",
|
"image": "https://cdn.discordapp.com/avatars/194584980922433536/7e8f880edc7b213bbb4ad02a50c9fb36.webp",
|
||||||
"class": "outline-blue-500 bg-black ",
|
"class": "outline-blue-500 bg-black ",
|
||||||
"coordinates": [69, 561],
|
"coordinates": [69, 561],
|
||||||
"size": 54
|
"size": 54
|
||||||
|
@ -201,7 +195,7 @@
|
||||||
"size": 49
|
"size": 49
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "https://cdn.discordapp.com/avatars/317785409763541002/8e8d743abd3f8aafc87ebd8484a57f26.webp",
|
"image": "https://cdn.discordapp.com/avatars/317785409763541002/4c6f2c001a577909b0904678c3309522.webp",
|
||||||
"class": "outline-blue-500 bg-black ",
|
"class": "outline-blue-500 bg-black ",
|
||||||
"coordinates": [119, 202],
|
"coordinates": [119, 202],
|
||||||
"size": 49
|
"size": 49
|
||||||
|
@ -213,7 +207,7 @@
|
||||||
"size": 69
|
"size": 69
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"image": "https://cdn.discordapp.com/avatars/706672850907430942/1e2a38423ce667a69e05c4ad8712ccab.webp",
|
"image": "https://cdn.discordapp.com/avatars/706672850907430942/5d5f32f579d9061c5392377fd334c1b3.webp",
|
||||||
"class": "outline-stone-500 bg-black ",
|
"class": "outline-stone-500 bg-black ",
|
||||||
"coordinates": [771, 818],
|
"coordinates": [771, 818],
|
||||||
"size": 49
|
"size": 49
|
||||||
|
|
|
@ -1,7 +1,22 @@
|
||||||
/* eslint-disable no-useless-escape */
|
/* eslint-disable no-useless-escape */
|
||||||
|
import {
|
||||||
|
interval,
|
||||||
|
map,
|
||||||
|
of,
|
||||||
|
scan,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
merge,
|
||||||
|
timeInterval,
|
||||||
|
filter,
|
||||||
|
take,
|
||||||
|
pipe as rxpipe,
|
||||||
|
Observable
|
||||||
|
} from 'rxjs'
|
||||||
|
|
||||||
import { inview } from 'svelte-inview'
|
import { inview } from 'svelte-inview'
|
||||||
import { pick } from 'remeda'
|
import { pick } from 'remeda'
|
||||||
import { Observable, debounceTime, share, startWith, throttleTime } from 'rxjs'
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fade: The initial opacity from 0 to 1.
|
* Fade: The initial opacity from 0 to 1.
|
||||||
|
@ -84,7 +99,7 @@ export function getIsMobile() {
|
||||||
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
|
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
|
||||||
a
|
a
|
||||||
) ||
|
) ||
|
||||||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
|
||||||
a.substr(0, 4)
|
a.substr(0, 4)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -131,3 +146,93 @@ export function trimText(text, maxLenght) {
|
||||||
export function getFileNameWithoutExtension(filePath) {
|
export function getFileNameWithoutExtension(filePath) {
|
||||||
return filePath.split('/').at(-1).replace(/\..*$/, '')
|
return filePath.split('/').at(-1).replace(/\..*$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom operator which maps inputs to their amount (level),
|
||||||
|
* and completes once a target level has been reached.
|
||||||
|
*
|
||||||
|
* On inactivity the level decreases (fallof).
|
||||||
|
*
|
||||||
|
* Used here to do fancy stuff with clicks
|
||||||
|
*/
|
||||||
|
export function createThresholdStream({ clicksTarget = 69, clicksEachMs = 400, fallof = 20 }) {
|
||||||
|
const FALLOF = -clicksTarget / fallof
|
||||||
|
|
||||||
|
return rxpipe(
|
||||||
|
timeInterval(),
|
||||||
|
filter(({ interval }) => interval < clicksEachMs),
|
||||||
|
map(() => 1),
|
||||||
|
switchMap((value) =>
|
||||||
|
merge(
|
||||||
|
of(value),
|
||||||
|
|
||||||
|
/** If no new value comes in, start decreasing the progress */
|
||||||
|
interval(clicksEachMs + 100).pipe(
|
||||||
|
take(clicksTarget), // Prevent this interval from running forever
|
||||||
|
map(() => FALLOF)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
scan((level, value) => Math.min(clicksTarget, Math.max(level + value, 0))),
|
||||||
|
startWith(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tell the browser to preload an image
|
||||||
|
*
|
||||||
|
* @param {string} src
|
||||||
|
*/
|
||||||
|
export function preloadImage(src) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const image = new Image()
|
||||||
|
image.src = src
|
||||||
|
image.onload = resolve
|
||||||
|
image.onerror = reject
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A writable store but as as an observable.
|
||||||
|
* Observables are much nicher than regular stores.
|
||||||
|
* @template T
|
||||||
|
* @param {T} init
|
||||||
|
* @returns {Observable<T> & { update: (updater: (state: T) => T) => void}}
|
||||||
|
*/
|
||||||
|
export function writableObservable(init) {
|
||||||
|
const { update, subscribe } = writable(init)
|
||||||
|
const observable = new Observable((subscriber) => {
|
||||||
|
const unsubscribe = subscribe((value) => subscriber.next(value))
|
||||||
|
|
||||||
|
return unsubscribe
|
||||||
|
})
|
||||||
|
observable.update = update
|
||||||
|
|
||||||
|
return observable
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a store to an observable
|
||||||
|
* @template T
|
||||||
|
* @param {import('svelte/store').Readable<T>} store
|
||||||
|
* @returns {Observable<T>}
|
||||||
|
*/
|
||||||
|
export function convertStoreToObservable(store) {
|
||||||
|
return new Observable((subscriber) => {
|
||||||
|
return store.subscribe((value) => subscriber.next(value))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if two rectangles are intersecting
|
||||||
|
* @param {{size: number, coordinates: [x: number, y: number]}} rect1
|
||||||
|
* @param {{size: number, coordinates: [x: number, y: number]} rect2
|
||||||
|
*/
|
||||||
|
export function isIntersecting(rect1, rect2) {
|
||||||
|
return !(
|
||||||
|
rect1.coordinates[0] + rect1.size < rect2.coordinates[0] ||
|
||||||
|
rect2.coordinates[0] + rect2.size < rect1.coordinates[0] ||
|
||||||
|
rect1.coordinates[1] + rect1.size < rect2.coordinates[1] ||
|
||||||
|
rect2.coordinates[1] + rect2.size < rect1.coordinates[1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
38
src/lib/Types.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import type { Observable } from 'rxjs'
|
||||||
|
|
||||||
|
export interface CommunityProfile {
|
||||||
|
image: string
|
||||||
|
coordinates: [number, number]
|
||||||
|
containerClass: string[]
|
||||||
|
size: number
|
||||||
|
quote?: string
|
||||||
|
onDragStart?: (event: DragEvent) => void
|
||||||
|
onDragEnd?: (event: DragEvent) => void
|
||||||
|
onHover?: (event: MouseEvent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommunityContext {
|
||||||
|
/** The size of the biggest profile picture. Non reactive for now */
|
||||||
|
biggestSize: number
|
||||||
|
/** The size of the smallest profile picture. Non reactive for now */
|
||||||
|
smallestSize: number
|
||||||
|
/** Get the HTML wrapper element / drag bounds element */
|
||||||
|
getSectionElement: () => HTMLElement
|
||||||
|
/** State for community profile stuff like interactions */
|
||||||
|
profilesState$: Observable<ProfilesState> & {
|
||||||
|
update: (updater: (state: ProfilesState) => ProfilesState) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfilesState {
|
||||||
|
/**
|
||||||
|
* Which profiles intersect with each other
|
||||||
|
* Alphabetically sorted, like `a-b`, not `b-a`, to prevent duplicates.
|
||||||
|
* No tuples, so that a set can look them up and delete them easily.
|
||||||
|
*/
|
||||||
|
intersections: `${string}-${string}`[]
|
||||||
|
/** All tagged profiles */
|
||||||
|
profiles: Record<string, { size: number; coordinates: [x: number, y: number] }>
|
||||||
|
/** Current active events. Currently not used, but maybe in the future */
|
||||||
|
events: string[]
|
||||||
|
}
|
|
@ -3,8 +3,9 @@
|
||||||
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
import { createEventDispatcher, getContext, onDestroy, onMount } from 'svelte'
|
||||||
import { spring } from 'svelte/motion'
|
import { spring } from 'svelte/motion'
|
||||||
import { contextId as ctxId } from '../../routes/home-slices/CommunitySlice.svelte'
|
import { contextId as ctxId } from '../../routes/home-slices/CommunitySlice.svelte'
|
||||||
import { lerp } from '$lib/Helper.mjs'
|
import { convertStoreToObservable, isIntersecting, lerp } from '$lib/Helper.mjs'
|
||||||
import { inview } from 'svelte-inview'
|
import { inview } from 'svelte-inview'
|
||||||
|
import { Subject, distinctUntilChanged, throttle, throttleTime } from 'rxjs'
|
||||||
|
|
||||||
/** @type {string} */
|
/** @type {string} */
|
||||||
export let image
|
export let image
|
||||||
|
@ -14,6 +15,8 @@
|
||||||
export let size
|
export let size
|
||||||
/** @type {[number, number]} */
|
/** @type {[number, number]} */
|
||||||
export let coordinates
|
export let coordinates
|
||||||
|
/** @type {string | undefined} User description */
|
||||||
|
export let tag = undefined
|
||||||
|
|
||||||
/** @type {string | undefined} */
|
/** @type {string | undefined} */
|
||||||
export let quote = undefined
|
export let quote = undefined
|
||||||
|
@ -28,12 +31,17 @@
|
||||||
export let imageWrapper
|
export let imageWrapper
|
||||||
/** @type {HTMLImageElement}*/
|
/** @type {HTMLImageElement}*/
|
||||||
export let imageElement
|
export let imageElement
|
||||||
|
/** @type {string|undefined}*/
|
||||||
|
export let style = undefined
|
||||||
|
export let hasDelay = true
|
||||||
|
export let spawnInstanly = false
|
||||||
|
|
||||||
const { biggestSize, getSectionElement } = getContext(contextId)
|
/** @type {import('$lib/Types.ts').CommunityContext} **/
|
||||||
|
const { biggestSize, getSectionElement, profilesState$ } = getContext(contextId)
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const relativeSize = size / biggestSize
|
const relativeSize = size / biggestSize
|
||||||
const delay = Math.pow(1 - size / biggestSize, 4) * 4654
|
const delay = hasDelay ? Math.pow(1 - size / biggestSize, 4) * 4654 : 0
|
||||||
const dragCoordinates = spring([0, 0], {
|
const dragCoordinates = spring([0, 0], {
|
||||||
damping: lerp(0.2, 0.03, relativeSize),
|
damping: lerp(0.2, 0.03, relativeSize),
|
||||||
stiffness: lerp(0.2, 0.01, relativeSize),
|
stiffness: lerp(0.2, 0.01, relativeSize),
|
||||||
|
@ -50,7 +58,21 @@
|
||||||
function onViewEnter() {
|
function onViewEnter() {
|
||||||
if (imageElement.__error) return
|
if (imageElement.__error) return
|
||||||
|
|
||||||
setTimeout(() => (hasEnteredView = true), 550)
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
hasEnteredView = true
|
||||||
|
if (tag && profilesState$) {
|
||||||
|
profilesState$.update((state) => {
|
||||||
|
// No drag yet, so we can just use the normal coordinates
|
||||||
|
state.profiles[tag] = { size, coordinates }
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('enteredView', { dragCoordinates, imageElement, element, delay })
|
||||||
|
},
|
||||||
|
spawnInstanly ? 0 : 550
|
||||||
|
)
|
||||||
|
|
||||||
// Only load the library if the element entered the view, to improve performance
|
// Only load the library if the element entered the view, to improve performance
|
||||||
import('interactjs').then(({ default: interact }) => {
|
import('interactjs').then(({ default: interact }) => {
|
||||||
|
@ -82,12 +104,51 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const draggedSubscription = convertStoreToObservable(dragCoordinates)
|
||||||
|
.pipe(throttleTime(80))
|
||||||
|
.subscribe((drag) => {
|
||||||
|
const displayedPosition = getDisplayedPosition(coordinates, drag)
|
||||||
|
dispatch('dragged', displayedPosition)
|
||||||
|
|
||||||
|
if (!tag) return
|
||||||
|
|
||||||
|
// This whole things looks so slow, but if it works. Feel free to PR nicer way though
|
||||||
|
profilesState$.update((state) => {
|
||||||
|
state.profiles[tag] = { coordinates: displayedPosition, size }
|
||||||
|
|
||||||
|
const otherIntersections = state.intersections.filter((pair) => !pair.includes(tag))
|
||||||
|
const thisIntersections = Object.entries(state.profiles)
|
||||||
|
.filter(
|
||||||
|
([otherTag, rectangle]) =>
|
||||||
|
otherTag !== tag &&
|
||||||
|
isIntersecting(rectangle, { size, coordinates: displayedPosition })
|
||||||
|
)
|
||||||
|
.map(([otherTag]) => [otherTag, tag].sort().join('-'))
|
||||||
|
const allIntersections = [...otherIntersections, ...thisIntersections]
|
||||||
|
|
||||||
|
state.intersections = allIntersections
|
||||||
|
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Nesecarry as the load image event might not get fired when its already loaded ( for example after a page reload )
|
// Nesecarry as the load image event might not get fired when its already loaded ( for example after a page reload )
|
||||||
hasImageLoaded = hasImageLoaded || imageElement.complete
|
hasImageLoaded = hasImageLoaded || imageElement.complete
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => interactionjs?.off())
|
onDestroy(() => {
|
||||||
|
draggedSubscription.unsubscribe()
|
||||||
|
interactionjs?.off()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {[x: number, y: number]}origin
|
||||||
|
* @param {[x: number, y: number]}dragCoordinates
|
||||||
|
*/
|
||||||
|
function getDisplayedPosition(origin, dragCoordinates) {
|
||||||
|
return [origin.at(0) + dragCoordinates.at(0), origin.at(1) + dragCoordinates.at(1)]
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -97,11 +158,11 @@
|
||||||
hasImageLoaded ? 'opacity-100' : 'opacity-0'
|
hasImageLoaded ? 'opacity-100' : 'opacity-0'
|
||||||
)}
|
)}
|
||||||
style:translate={coordinates.map((xy) => xy + 'px').join(' ')}
|
style:translate={coordinates.map((xy) => xy + 'px').join(' ')}
|
||||||
style="width: {size}px; height: {size}px;--delay: {delay}ms;"
|
style="width: {size}px; height: {size}px;--delay: {delay}ms; "
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
>
|
>
|
||||||
<div
|
<button
|
||||||
class={clsx(
|
class={clsx(
|
||||||
'group absolute inset-0 h-full w-full touch-none select-none',
|
'group absolute inset-0 h-full w-full touch-none select-none',
|
||||||
isAnimating && 'opacity-0'
|
isAnimating && 'opacity-0'
|
||||||
|
@ -110,10 +171,11 @@
|
||||||
use:inview={{ unobserveOnEnter: true, threshold: 0.2 }}
|
use:inview={{ unobserveOnEnter: true, threshold: 0.2 }}
|
||||||
class:_animate={hasImageLoaded && isAnimating && hasEnteredView}
|
class:_animate={hasImageLoaded && isAnimating && hasEnteredView}
|
||||||
on:inview_enter={onViewEnter}
|
on:inview_enter={onViewEnter}
|
||||||
|
on:click
|
||||||
>
|
>
|
||||||
<div class="" bind:this={imageWrapper}>
|
<div class="" bind:this={imageWrapper}>
|
||||||
<img
|
<img
|
||||||
class="group h-full w-full touch-none select-none rounded-[50%] object-cover outline outline-4 {$$restProps.class}"
|
class="group aspect-square h-full w-full touch-none select-none rounded-[50%] object-cover outline outline-4 {$$restProps.class}"
|
||||||
bind:this={imageElement}
|
bind:this={imageElement}
|
||||||
on:load={() => (hasImageLoaded = true)}
|
on:load={() => (hasImageLoaded = true)}
|
||||||
src={image}
|
src={image}
|
||||||
|
@ -127,6 +189,7 @@
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
onerror="this.__error = true"
|
onerror="this.__error = true"
|
||||||
|
{style}
|
||||||
/>
|
/>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,7 +199,7 @@
|
||||||
{quote}
|
{quote}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
|
BIN
src/lib/images/poz/amongpoz.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/lib/images/poz/chinesepoz.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/lib/images/poz/discordpoz.webp
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/lib/images/poz/discordpoz_better.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/lib/images/poz/firepoz.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/lib/images/poz/gimppoz.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/lib/images/poz/jaceklord.webp
Normal file
After Width: | Height: | Size: 3 KiB |
BIN
src/lib/images/poz/jacekpoz.gif
Normal file
After Width: | Height: | Size: 1.1 MiB |
BIN
src/lib/images/poz/krolewnapoz.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/lib/images/poz/marcelinapoz.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/lib/images/poz/msedgepoz.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
src/lib/images/poz/nixpiss.webp
Normal file
After Width: | Height: | Size: 2.1 KiB |
1
src/lib/images/poz/poz eye.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><mask id="prefix__a" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="32" height="32"><path d="M0 0h32v32H0V0z" fill="#fff"/></mask><g mask="url(#prefix__a)"><path d="M31.103 20.992c0 2.01-.525 3.436-1.345 4.475-.827 1.05-2.016 1.784-3.476 2.29-2.961 1.029-6.772 1.039-10.182 1.039s-7.273-.01-10.287-1.04c-1.487-.508-2.701-1.244-3.546-2.296-.835-1.04-1.37-2.463-1.37-4.468 0-3.748.807-8.31 3.108-11.903 2.267-3.541 6.013-6.2 12.095-6.2 5.764 0 9.469 2.664 11.771 6.232 2.331 3.613 3.232 8.18 3.232 11.871z" fill="#FBC546" stroke="#000" stroke-width="1.794"/><path fill-rule="evenodd" clip-rule="evenodd" d="M19.454 22.413a.853.853 0 010 1.706H12.63a.853.853 0 010-1.706h6.825z" fill="#593604"/><path fill-rule="evenodd" clip-rule="evenodd" d="M27.133 19.853c0-.943-.764-1.706-1.706-1.706h-3.413a1.706 1.706 0 100 3.412h3.413c.942 0 1.706-.763 1.706-1.706zM11.776 19.853c0-.943-.764-1.706-1.706-1.706H6.657a1.706 1.706 0 100 3.412h3.413c.942 0 1.706-.763 1.706-1.706z" fill="#ED6B44"/><path fill-rule="evenodd" clip-rule="evenodd" d="M7.293 14.654c-.086-.127-.293-.11-.293.044v.002c0 .938.76 1.698 1.698 1.698h.017c.937 0 1.698-.76 1.698-1.698v-.002c0-.153-.207-.17-.293-.044-.306.449-.82.744-1.405.744h-.017c-.584 0-1.1-.295-1.405-.744z" fill="#000"/><path d="M23.15 9.586a.825.825 0 010 1.358c-1.318.96-4.025 2.594-7.15 2.594s-5.832-1.635-7.15-2.594a.825.825 0 010-1.358c1.318-.96 4.025-2.594 7.15-2.594s5.832 1.635 7.15 2.594z" fill="#fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M21.293 14.654c-.086-.127-.293-.11-.293.044v.002c0 .938.76 1.698 1.698 1.698h.017c.937 0 1.698-.76 1.698-1.698v-.002c0-.153-.207-.17-.293-.044-.306.449-.82.744-1.405.744h-.017c-.585 0-1.1-.295-1.405-.744z" fill="#000"/><mask id="prefix__b" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="10" y="6" width="12" height="8"><path d="M20.306 7.947c2.158.936 2.158 3.69 0 4.626-1.244.54-2.721.96-4.306.96s-3.062-.42-4.306-.96c-2.158-.937-2.158-3.69 0-4.626 1.244-.54 2.721-.96 4.306-.96s3.062.42 4.306.96z" fill="#fff"/></mask><g mask="url(#prefix__b)" fill-rule="evenodd" clip-rule="evenodd"><path d="M12.515 9.47a3.468 3.468 0 013.468-3.468h.034a3.468 3.468 0 013.468 3.467v.006a3.468 3.468 0 01-3.468 3.467h-.034a3.468 3.468 0 01-3.468-3.467v-.006z" fill="#FBC546"/><path d="M13.958 9.47c0-1.127.91-2.04 2.032-2.04h.02c1.122 0 2.032.913 2.032 2.04v.004c0 1.127-.91 2.04-2.032 2.04h-.02a2.037 2.037 0 01-2.032-2.04V9.47z" fill="#000"/></g></g></svg>
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/lib/images/poz/redditpoz.webp
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/lib/images/poz/slimakpoz.webp
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/lib/images/poz/teamspoz.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/lib/images/poz/trollpoz.webp
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/lib/images/poz/windowspoz.webp
Normal file
After Width: | Height: | Size: 1.5 KiB |
|
@ -12,16 +12,20 @@
|
||||||
import amongUsGreenImage from '$lib/images/amongus/green.webp'
|
import amongUsGreenImage from '$lib/images/amongus/green.webp'
|
||||||
import { discordLink } from '$lib/constants.mjs'
|
import { discordLink } from '$lib/constants.mjs'
|
||||||
import profiles from '../../content/profiles.json'
|
import profiles from '../../content/profiles.json'
|
||||||
|
import Poz from './community/Poz.svelte'
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { writableObservable } from '$lib/Helper.mjs'
|
||||||
|
|
||||||
let sectionElement
|
let sectionElement
|
||||||
let isDraggingChan = false
|
let isDraggingChan = false
|
||||||
|
|
||||||
const validSizes = [16, 20, 24, 32, 40, 48, 64, 80, 96, 100, 128, 160, 240, 320, 640]
|
const validSizes = [16, 20, 24, 32, 40, 48, 64, 80, 96, 100, 128, 160, 240, 320, 640]
|
||||||
|
|
||||||
/** @type {Promise<import('./Types').CommunityProfile[]>}*/
|
/** @type {Promise<import('$lib/Types').CommunityProfile[]>}*/
|
||||||
let allProfilesPromise = new Promise(() => {})
|
let allProfilesPromise = new Promise(() => {})
|
||||||
|
|
||||||
/** @type {import('./Types').CommunityProfile[]} */
|
/** @type {import('$lib/Types').CommunityProfile[]} */
|
||||||
const extraProfiles = [
|
const extraProfiles = [
|
||||||
{
|
{
|
||||||
image: 'imgs/chan/joy.svg',
|
image: 'imgs/chan/joy.svg',
|
||||||
|
@ -39,14 +43,6 @@
|
||||||
onHover: ({ detail: { srcElement } }) =>
|
onHover: ({ detail: { srcElement } }) =>
|
||||||
!isDraggingChan && (srcElement.src = 'imgs/chan/wink.svg')
|
!isDraggingChan && (srcElement.src = 'imgs/chan/wink.svg')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// jacekpoz
|
|
||||||
image: '/imgs/profile_pictures/jacekpoz.svg',
|
|
||||||
coordinates: [893, 622],
|
|
||||||
size: 80,
|
|
||||||
class: 'outline-yellow-500 bg-black ',
|
|
||||||
quote: '"piss blob"'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
image: amongUsGreenImage,
|
image: amongUsGreenImage,
|
||||||
coordinates: [873, 224],
|
coordinates: [873, 224],
|
||||||
|
@ -88,7 +84,8 @@
|
||||||
(previousSize, { size }) => (size < previousSize ? size : previousSize),
|
(previousSize, { size }) => (size < previousSize ? size : previousSize),
|
||||||
Number.POSITIVE_INFINITY
|
Number.POSITIVE_INFINITY
|
||||||
),
|
),
|
||||||
getSectionElement: () => sectionElement
|
getSectionElement: () => sectionElement,
|
||||||
|
profilesState$: writableObservable({ events: [], intersections: [], profiles: {} })
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -143,6 +140,8 @@
|
||||||
on:hover={onHover}
|
on:hover={onHover}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<Poz />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
export interface CommunityProfile {
|
|
||||||
image: string
|
|
||||||
coordinates: [number, number]
|
|
||||||
containerClass: string[]
|
|
||||||
size: number
|
|
||||||
quote?: string
|
|
||||||
onDragStart?: (event: DragEvent) => void
|
|
||||||
onDragEnd?: (event: DragEvent) => void
|
|
||||||
onHover?: (event: MouseEvent) => void
|
|
||||||
}
|
|
103
src/routes/home-slices/community/Poz.svelte
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
<script>
|
||||||
|
import { createThresholdStream, lerp, preloadImage } from '$lib/Helper.mjs'
|
||||||
|
import DiscordProfilePicture from '$lib/components/DiscordProfilePicture.svelte'
|
||||||
|
import { Subject, filter, first, map, merge, of, startWith, switchMap, timer } from 'rxjs'
|
||||||
|
import edgePoz from '$lib/images/poz/msedgepoz.webp'
|
||||||
|
import { getContext, onDestroy } from 'svelte'
|
||||||
|
import { contextId } from '../CommunitySlice.svelte'
|
||||||
|
|
||||||
|
const thePozArmy = Object.values(import.meta.glob('$lib/images/poz/*', { eager: true })).map(
|
||||||
|
(x) => x.default
|
||||||
|
)
|
||||||
|
|
||||||
|
/** @type {import('$lib/Types').CommunityContext}*/
|
||||||
|
const { profilesState$ } = getContext(contextId)
|
||||||
|
$: touches$$$$Voice = $profilesState$.intersections.includes('le_mod-poz')
|
||||||
|
|
||||||
|
const origin = [893, 622]
|
||||||
|
let newPosition
|
||||||
|
const clicksTarget = 9
|
||||||
|
const shakeMax = 24
|
||||||
|
const clicksInput$ = new Subject()
|
||||||
|
const level$ = clicksInput$.pipe(
|
||||||
|
createThresholdStream({ clicksTarget, clicksEachMs: 250, fallof: 10 })
|
||||||
|
)
|
||||||
|
const relativeLevel$ = level$.pipe(map((clicks) => clicks / clicksTarget))
|
||||||
|
const hasFinished$ = relativeLevel$.pipe(
|
||||||
|
filter((clicks) => clicks >= 1),
|
||||||
|
first(),
|
||||||
|
map(() => true),
|
||||||
|
startWith(false)
|
||||||
|
)
|
||||||
|
const showMainPoz$ = hasFinished$.pipe(
|
||||||
|
filter((is) => is === true),
|
||||||
|
switchMap(() => timer(550)),
|
||||||
|
first(),
|
||||||
|
map(() => false),
|
||||||
|
startWith(true)
|
||||||
|
)
|
||||||
|
const shake$ = relativeLevel$.pipe(
|
||||||
|
switchMap((progress) => {
|
||||||
|
const shakeModifier = shakeMax * Math.max(progress, 0.01)
|
||||||
|
return merge(
|
||||||
|
of({
|
||||||
|
x: Math.random() * shakeModifier,
|
||||||
|
y: Math.random() * shakeModifier
|
||||||
|
}),
|
||||||
|
// Reset back to the original value after 140ms
|
||||||
|
timer(140).pipe(map(() => ({ x: 0, y: 0 })))
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
startWith({ x: 0, y: 0 })
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preload images when the user start clicking our beloved Poz
|
||||||
|
const preloadSubscription = relativeLevel$
|
||||||
|
.pipe(
|
||||||
|
filter((level) => level >= 0.1),
|
||||||
|
first()
|
||||||
|
)
|
||||||
|
.subscribe(() => thePozArmy.forEach(preloadImage))
|
||||||
|
|
||||||
|
onDestroy(() => preloadSubscription.unsubscribe())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $hasFinished$}
|
||||||
|
{#each thePozArmy as poz}
|
||||||
|
{@const maxSize = 75}
|
||||||
|
{@const size = 35 * Math.random() + 40}
|
||||||
|
<DiscordProfilePicture
|
||||||
|
image={poz}
|
||||||
|
coordinates={newPosition ?? origin}
|
||||||
|
{size}
|
||||||
|
class={'bg-black/50 outline-yellow-500 '}
|
||||||
|
spawnInstantly={false}
|
||||||
|
isAnimating={false}
|
||||||
|
tag="poz"
|
||||||
|
on:enteredView={({ detail: { dragCoordinates } }) => {
|
||||||
|
dragCoordinates.update(([x, y]) => {
|
||||||
|
x += lerp(400, 0, (size / maxSize) * (1 - Math.random())) * (Math.random() > 0.5 ? 1 : -1)
|
||||||
|
y += lerp(400, 0, (size / maxSize) * (1 - Math.random())) * (Math.random() > 0.5 ? 1 : -1)
|
||||||
|
return [x, y]
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $showMainPoz$}
|
||||||
|
<div class="absolute z-20">
|
||||||
|
<DiscordProfilePicture
|
||||||
|
image={touches$$$$Voice ? edgePoz : '/imgs/profile_pictures/jacekpoz.svg'}
|
||||||
|
coordinates={origin}
|
||||||
|
size={80}
|
||||||
|
class={'bg-black outline-yellow-500 '}
|
||||||
|
quote={'"piss blob"'}
|
||||||
|
intersectionHandler={(image) => (image = '"piss blob"')}
|
||||||
|
tag="poz"
|
||||||
|
on:click={() => clicksInput$.next(0)}
|
||||||
|
style={`scale:${$relativeLevel$ * 0.5 + 1};transition: scale 80ms linear; translate: ${$shake$.x}px ${$shake$.y}px; `}
|
||||||
|
on:dragged={({ detail }) => (newPosition = detail)}
|
||||||
|
></DiscordProfilePicture>
|
||||||
|
</div>
|
||||||
|
{/if}
|