mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
chore(repo): remove dead code and useless tests (#3375)
This commit is contained in:
1 parent
7e177a1c90
commit
1cd62eb661
1391 files changed
+783
-120415
No files matched your search
@@ -1,425 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {mkdir, readFile, writeFile} from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import {performance} from 'node:perf_hooks';
|
||||
import {isMainThread, parentPort, Worker, workerData} from 'node:worker_threads';
|
||||
import {encode as encodePng} from 'fast-png';
|
||||
import {encode as encodeJpeg} from 'jpeg-js';
|
||||
import {
|
||||
assemble_apng_frames,
|
||||
assemble_gif_frame_chunks,
|
||||
crop_and_rotate_apng,
|
||||
crop_and_rotate_gif,
|
||||
crop_and_rotate_image,
|
||||
crop_rotate_rgba,
|
||||
decode_apng_frames,
|
||||
decode_gif_frames,
|
||||
encode_apng_frame_payload,
|
||||
encode_apng_frames,
|
||||
encode_gif_frame_chunk,
|
||||
encode_gif_frames,
|
||||
initSync,
|
||||
is_animated_image,
|
||||
} from '../pkgs/libfluxcore/libfluxcore.js';
|
||||
|
||||
const wasmUrl = new URL('../pkgs/libfluxcore/libfluxcore_bg.wasm', import.meta.url);
|
||||
const mediaCacheDir = new URL('../.cache/libfluxcore-bench-media/', import.meta.url);
|
||||
const workerCount = Math.max(1, Math.min(4, os.availableParallelism?.() ?? os.cpus().length ?? 1));
|
||||
const profile = process.argv.find((arg) => arg.startsWith('--profile='))?.slice('--profile='.length) ?? 'standard';
|
||||
const downloadRealMediaOnly = process.argv.includes('--download-realmedia');
|
||||
const offlineRealMedia = process.argv.includes('--offline') || process.env.FLUXCORE_BENCH_OFFLINE === '1';
|
||||
const userAgent = 'Fluxer libfluxcore benchmark (https://fluxer.app)';
|
||||
const realMediaAssets = [
|
||||
{
|
||||
id: 'jpeg-fronalpstock',
|
||||
format: 'jpeg',
|
||||
fileName: 'fronalpstock_big.jpg',
|
||||
expectedBytes: 14_679_474,
|
||||
url: 'https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg',
|
||||
source: 'https://commons.wikimedia.org/wiki/File:Fronalpstock_big.jpg',
|
||||
},
|
||||
{
|
||||
id: 'png-snr-demo',
|
||||
format: 'png',
|
||||
fileName: 'snr_image_demonstration.png',
|
||||
expectedBytes: 1_146_718,
|
||||
url: 'https://upload.wikimedia.org/wikipedia/commons/f/f9/SNR_image_demonstration.png',
|
||||
source: 'https://commons.wikimedia.org/wiki/File:SNR_image_demonstration.png',
|
||||
},
|
||||
{
|
||||
id: 'gif-gerridae',
|
||||
format: 'gif',
|
||||
fileName: 'gerridae_1200x675.gif',
|
||||
expectedBytes: 6_318_395,
|
||||
url: 'https://upload.wikimedia.org/wikipedia/commons/f/f0/%22%2Barya%2B%22_Gerridae_-_Anggang_kayak_-_laba-laba_air_-_Lengkongwetan_2020_1.gif',
|
||||
source:
|
||||
'https://commons.wikimedia.org/wiki/File:%22%2Barya%2B%22_Gerridae_-_Anggang_kayak_-_laba-laba_air_-_Lengkongwetan_2020_1.gif',
|
||||
},
|
||||
{
|
||||
id: 'apng-human-male',
|
||||
format: 'apng',
|
||||
fileName: '201803_human_male_anim.png',
|
||||
expectedBytes: 8_150_411,
|
||||
url: 'https://upload.wikimedia.org/wikipedia/commons/c/c7/201803_Human_Male_anim.png',
|
||||
source: 'https://commons.wikimedia.org/wiki/File:201803_Human_Male_anim.png',
|
||||
},
|
||||
{
|
||||
id: 'webp-samsung-note',
|
||||
format: 'webp',
|
||||
fileName: 'samsung_galaxy_note.webp',
|
||||
expectedBytes: 7_326_306,
|
||||
url: 'https://upload.wikimedia.org/wikipedia/commons/5/59/Samsung_Galaxy_Note.WebP',
|
||||
source: 'https://commons.wikimedia.org/wiki/File:Samsung_Galaxy_Note.WebP',
|
||||
},
|
||||
{
|
||||
id: 'avif-hato',
|
||||
format: 'avif',
|
||||
fileName: 'hato.profile0.8bpc.yuv420.avif',
|
||||
expectedBytes: 259_104,
|
||||
url: 'https://raw.githubusercontent.com/link-u/avif-sample-images/master/hato.profile0.8bpc.yuv420.avif',
|
||||
source: 'https://github.com/link-u/avif-sample-images',
|
||||
},
|
||||
];
|
||||
|
||||
function initWasm(bytes) {
|
||||
initSync({module: bytes});
|
||||
}
|
||||
|
||||
function makeFrame(width, height, seed) {
|
||||
const rgba = new Uint8Array(width * height * 4);
|
||||
for (let index = 0, pixel = 0; index < rgba.length; index += 4, pixel += 1) {
|
||||
const x = pixel % width;
|
||||
const y = Math.floor(pixel / width);
|
||||
rgba[index] = (x * 3 + seed * 17) & 0xff;
|
||||
rgba[index + 1] = (y * 5 + seed * 29) & 0xff;
|
||||
rgba[index + 2] = ((x ^ y) + seed * 11) & 0xff;
|
||||
rgba[index + 3] = 255;
|
||||
}
|
||||
return {rgba, width, height, delayMs: 40};
|
||||
}
|
||||
|
||||
function batches(frames, count) {
|
||||
const out = Array.from({length: count}, () => []);
|
||||
for (let index = 0; index < frames.length; index += 1) out[index % count].push({frame: frames[index], index});
|
||||
return out.filter((batch) => batch.length > 0);
|
||||
}
|
||||
|
||||
async function encodeInWorkers(kind, frames, wasmBytes, options = {}) {
|
||||
const jobs = batches(frames, Math.min(workerCount, frames.length));
|
||||
const workers = jobs.map(() => new Worker(new URL(import.meta.url), {workerData: {wasmBytes}}));
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
jobs.map(
|
||||
(batch, index) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const worker = workers[index];
|
||||
const payload = batch.map(({frame, index: frameIndex}) => ({
|
||||
frame: {
|
||||
rgba: options.transferFrames ? frame.rgba : frame.rgba.slice(),
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
delayMs: frame.delayMs,
|
||||
},
|
||||
index: frameIndex,
|
||||
}));
|
||||
worker.once('message', (message) => {
|
||||
if (message.ok) resolve(message.frames);
|
||||
else reject(new Error(message.error || 'worker encode failed'));
|
||||
});
|
||||
worker.once('error', reject);
|
||||
worker.postMessage(
|
||||
{kind, frames: payload},
|
||||
payload.map(({frame}) => frame.rgba.buffer),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
const ordered = new Array(frames.length);
|
||||
for (const result of results) {
|
||||
for (const item of result) ordered[item.index] = item.frame;
|
||||
}
|
||||
if (kind === 'apng') return assemble_apng_frames(ordered);
|
||||
return assemble_gif_frame_chunks(ordered);
|
||||
} finally {
|
||||
await Promise.allSettled(workers.map((worker) => worker.terminate()));
|
||||
}
|
||||
}
|
||||
|
||||
async function transformInWorkers(frames, cropParams, wasmBytes) {
|
||||
const jobs = batches(frames, Math.min(workerCount, frames.length));
|
||||
const workers = jobs.map(() => new Worker(new URL(import.meta.url), {workerData: {wasmBytes}}));
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
jobs.map(
|
||||
(batch, index) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const worker = workers[index];
|
||||
const payload = batch.map(({frame, index: frameIndex}) => ({
|
||||
frame: {
|
||||
rgba: frame.rgba,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
delayMs: frame.delayMs,
|
||||
},
|
||||
index: frameIndex,
|
||||
}));
|
||||
worker.once('message', (message) => {
|
||||
if (message.ok) resolve(message.frames);
|
||||
else reject(new Error(message.error || 'worker transform failed'));
|
||||
});
|
||||
worker.once('error', reject);
|
||||
worker.postMessage(
|
||||
{kind: 'transform', frames: payload, cropParams},
|
||||
payload.map(({frame}) => frame.rgba.buffer),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
const ordered = new Array(frames.length);
|
||||
for (const result of results) {
|
||||
for (const item of result) ordered[item.index] = item.frame;
|
||||
}
|
||||
return ordered;
|
||||
} finally {
|
||||
await Promise.allSettled(workers.map((worker) => worker.terminate()));
|
||||
}
|
||||
}
|
||||
|
||||
function benchSync(label, iterations, fn) {
|
||||
fn();
|
||||
const start = performance.now();
|
||||
let result;
|
||||
for (let index = 0; index < iterations; index += 1) result = fn();
|
||||
const elapsed = performance.now() - start;
|
||||
const bytes = result?.byteLength ?? result?.rgba?.byteLength ?? 0;
|
||||
console.log(`${label}: ${(elapsed / iterations).toFixed(2)} ms/op (${iterations} iters, ${bytes} bytes last result)`);
|
||||
}
|
||||
|
||||
async function benchAsync(label, iterations, fn) {
|
||||
await fn();
|
||||
const start = performance.now();
|
||||
let result;
|
||||
for (let index = 0; index < iterations; index += 1) result = await fn();
|
||||
const elapsed = performance.now() - start;
|
||||
const bytes = result?.byteLength ?? 0;
|
||||
console.log(`${label}: ${(elapsed / iterations).toFixed(2)} ms/op (${iterations} iters, ${bytes} bytes last result)`);
|
||||
}
|
||||
|
||||
function benchOnce(label, fn) {
|
||||
const start = performance.now();
|
||||
const result = fn();
|
||||
const elapsed = performance.now() - start;
|
||||
const bytes = result?.byteLength ?? result?.rgba?.byteLength ?? 0;
|
||||
console.log(`${label}: ${elapsed.toFixed(2)} ms (${bytes} bytes result)`);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function benchOnceAsync(label, fn) {
|
||||
const start = performance.now();
|
||||
const result = await fn();
|
||||
const elapsed = performance.now() - start;
|
||||
const bytes = result?.byteLength ?? result?.rgba?.byteLength ?? 0;
|
||||
console.log(`${label}: ${elapsed.toFixed(2)} ms (${bytes} bytes result)`);
|
||||
return result;
|
||||
}
|
||||
|
||||
function assertExpectedAssetSize(asset, bytes) {
|
||||
if (asset.expectedBytes != null && bytes.byteLength !== asset.expectedBytes) {
|
||||
throw new Error(`${asset.id} expected ${asset.expectedBytes} bytes, got ${bytes.byteLength}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function readOrDownloadAsset(asset, options = {}) {
|
||||
await mkdir(mediaCacheDir, {recursive: true});
|
||||
const fileUrl = new URL(asset.fileName, mediaCacheDir);
|
||||
try {
|
||||
const data = await readFile(fileUrl);
|
||||
assertExpectedAssetSize(asset, data);
|
||||
console.log(`asset ${asset.id}: cache hit (${data.byteLength} bytes)`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (options.offline) {
|
||||
throw new Error(
|
||||
`missing or invalid local media asset ${asset.fileName}; run pnpm wasm:bench:download-realmedia`,
|
||||
{cause: error},
|
||||
);
|
||||
}
|
||||
}
|
||||
const response = await fetch(asset.url, {headers: {'User-Agent': userAgent}});
|
||||
if (!response.ok) throw new Error(`failed to download ${asset.id}: HTTP ${response.status}`);
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
assertExpectedAssetSize(asset, bytes);
|
||||
await writeFile(fileUrl, bytes);
|
||||
console.log(`asset ${asset.id}: downloaded ${bytes.byteLength} bytes from ${asset.source}`);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async function loadRealMediaAssets(options = {}) {
|
||||
const entries = await Promise.all(
|
||||
realMediaAssets.map(async (asset) => [asset.id, {...asset, bytes: await readOrDownloadAsset(asset, options)}]),
|
||||
);
|
||||
return new Map(entries);
|
||||
}
|
||||
|
||||
async function runStandardProfile(wasmBytes) {
|
||||
const cropSource = makeFrame(1024, 1024, 1);
|
||||
benchSync('crop_rotate_rgba 1024x1024 -> 512x512 rotate90', 60, () =>
|
||||
crop_rotate_rgba(cropSource.rgba, cropSource.width, cropSource.height, 64, 64, 768, 768, 90, 512, 512),
|
||||
);
|
||||
const apngFrames = Array.from({length: 24}, (_, index) => makeFrame(320, 320, index));
|
||||
benchSync('encode_apng_frames serial 24x320', 4, () => encode_apng_frames(apngFrames));
|
||||
await benchAsync('encode_apng_frames worker payloads 24x320', 4, () =>
|
||||
encodeInWorkers('apng', apngFrames, wasmBytes),
|
||||
);
|
||||
const gifFrames = Array.from({length: 16}, (_, index) => makeFrame(192, 192, index));
|
||||
benchSync('encode_gif_frames serial 16x192', 3, () => encode_gif_frames(gifFrames));
|
||||
await benchAsync('encode_gif_frames worker chunks 16x192', 3, () => encodeInWorkers('gif', gifFrames, wasmBytes));
|
||||
}
|
||||
|
||||
async function runHighResProfile(wasmBytes) {
|
||||
const crop4k = makeFrame(3840, 2160, 11);
|
||||
benchSync('crop_rotate_rgba 4K -> 1080p rotate90', 10, () =>
|
||||
crop_rotate_rgba(crop4k.rgba, crop4k.width, crop4k.height, 420, 120, 3000, 1800, 90, 1080, 1920),
|
||||
);
|
||||
const staticFrame = makeFrame(1920, 1080, 17);
|
||||
const pngSource = encodePng({
|
||||
width: staticFrame.width,
|
||||
height: staticFrame.height,
|
||||
data: staticFrame.rgba,
|
||||
depth: 8,
|
||||
channels: 4,
|
||||
});
|
||||
const jpegSource = encodeJpeg(
|
||||
{width: staticFrame.width, height: staticFrame.height, data: staticFrame.rgba},
|
||||
88,
|
||||
).data;
|
||||
benchSync('crop_and_rotate_image PNG 1080p -> PNG 720p', 3, () =>
|
||||
crop_and_rotate_image(pngSource, 'png', 160, 90, 1600, 900, 0, 1280, 720),
|
||||
);
|
||||
benchSync('crop_and_rotate_image JPEG 1080p -> JPEG 720p', 3, () =>
|
||||
crop_and_rotate_image(jpegSource, 'jpeg', 160, 90, 1600, 900, 0, 1280, 720),
|
||||
);
|
||||
const apngFrames = Array.from({length: 6}, (_, index) => makeFrame(1920, 1080, index + 31));
|
||||
benchSync('encode_apng_frames serial 6x1080p', 1, () => encode_apng_frames(apngFrames));
|
||||
await benchAsync('encode_apng_frames worker payloads 6x1080p', 1, () =>
|
||||
encodeInWorkers('apng', apngFrames, wasmBytes),
|
||||
);
|
||||
const gifFrames = Array.from({length: 6}, (_, index) => makeFrame(1280, 720, index + 51));
|
||||
benchSync('encode_gif_frames serial 6x720p', 1, () => encode_gif_frames(gifFrames));
|
||||
await benchAsync('encode_gif_frames worker chunks 6x720p', 1, () => encodeInWorkers('gif', gifFrames, wasmBytes));
|
||||
}
|
||||
|
||||
async function runRealMediaProfile(wasmBytes) {
|
||||
const assets = await loadRealMediaAssets({offline: offlineRealMedia});
|
||||
const jpeg = assets.get('jpeg-fronalpstock').bytes;
|
||||
const png = assets.get('png-snr-demo').bytes;
|
||||
const gif = assets.get('gif-gerridae').bytes;
|
||||
const apng = assets.get('apng-human-male').bytes;
|
||||
const webp = assets.get('webp-samsung-note').bytes;
|
||||
const avif = assets.get('avif-hato').bytes;
|
||||
benchOnce('real JPEG crop Fronalpstock 10109x4542 -> JPEG 1920x1080', () =>
|
||||
crop_and_rotate_image(jpeg, 'jpeg', 1000, 400, 8000, 3600, 0, 1920, 1080),
|
||||
);
|
||||
benchOnce('real PNG crop SNR demo 3840x2880 -> PNG 1280x720', () =>
|
||||
crop_and_rotate_image(png, 'png', 320, 240, 3200, 1800, 0, 1280, 720),
|
||||
);
|
||||
benchOnce('real APNG serial crop 1920x1920x22 -> 720x720', () =>
|
||||
crop_and_rotate_apng(apng, 240, 240, 1440, 1440, 0, 720, 720),
|
||||
);
|
||||
await benchOnceAsync('real APNG worker-style crop 1920x1920x22 -> 720x720', async () => {
|
||||
const frames = decode_apng_frames(apng);
|
||||
const transformed = await transformInWorkers(
|
||||
frames,
|
||||
{x: 240, y: 240, width: 1440, height: 1440, rotation: 0, resizeWidth: 720, resizeHeight: 720},
|
||||
wasmBytes,
|
||||
);
|
||||
return encodeInWorkers('apng', transformed, wasmBytes, {transferFrames: true});
|
||||
});
|
||||
benchOnce('real GIF serial crop 1200x675x13 -> 854x480', () =>
|
||||
crop_and_rotate_gif(gif, 80, 45, 1040, 585, 0, 854, 480),
|
||||
);
|
||||
await benchOnceAsync('real GIF worker-style crop 1200x675x13 -> 854x480', async () => {
|
||||
const frames = decode_gif_frames(gif);
|
||||
const transformed = await transformInWorkers(
|
||||
frames,
|
||||
{x: 80, y: 45, width: 1040, height: 585, rotation: 0, resizeWidth: 854, resizeHeight: 480},
|
||||
wasmBytes,
|
||||
);
|
||||
return encodeInWorkers('gif', transformed, wasmBytes, {transferFrames: true});
|
||||
});
|
||||
benchSync('real WebP animated detection 4032x3024', 1000, () => (is_animated_image(webp) ? webp : webp));
|
||||
benchSync('real AVIF animated detection 3082x2048', 1000, () => (is_animated_image(avif) ? avif : avif));
|
||||
console.log(
|
||||
'WebP and AVIF decode/crop are routed through browser ImageDecoder or the native media bridge, not this Node wasm loader.',
|
||||
);
|
||||
}
|
||||
|
||||
if (!isMainThread) {
|
||||
initWasm(workerData.wasmBytes);
|
||||
parentPort.on('message', (message) => {
|
||||
try {
|
||||
if (message.kind === 'apng') {
|
||||
const frames = message.frames.map(({frame, index}) => ({
|
||||
frame: encode_apng_frame_payload(frame),
|
||||
index,
|
||||
}));
|
||||
parentPort.postMessage(
|
||||
{ok: true, frames},
|
||||
frames.map(({frame}) => frame.compressed.buffer),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (message.kind === 'transform') {
|
||||
const frames = message.frames.map(({frame, index}) => {
|
||||
const transformed = crop_rotate_rgba(
|
||||
frame.rgba,
|
||||
frame.width,
|
||||
frame.height,
|
||||
message.cropParams.x,
|
||||
message.cropParams.y,
|
||||
message.cropParams.width,
|
||||
message.cropParams.height,
|
||||
message.cropParams.rotation,
|
||||
message.cropParams.resizeWidth,
|
||||
message.cropParams.resizeHeight,
|
||||
);
|
||||
return {frame: {...transformed, delayMs: frame.delayMs}, index};
|
||||
});
|
||||
parentPort.postMessage(
|
||||
{ok: true, frames},
|
||||
frames.map(({frame}) => frame.rgba.buffer),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const frames = message.frames.map(({frame, index}) => ({
|
||||
frame: encode_gif_frame_chunk(frame, index === 0),
|
||||
index,
|
||||
}));
|
||||
parentPort.postMessage(
|
||||
{ok: true, frames},
|
||||
frames.map(({frame}) => frame.data.buffer),
|
||||
);
|
||||
} catch (error) {
|
||||
parentPort.postMessage({ok: false, error: error instanceof Error ? error.message : String(error)});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (downloadRealMediaOnly) {
|
||||
await loadRealMediaAssets({offline: false});
|
||||
console.log(`real media assets are local in ${mediaCacheDir.pathname}`);
|
||||
process.exit(0);
|
||||
}
|
||||
const wasmBytes = await readFile(wasmUrl);
|
||||
initWasm(wasmBytes);
|
||||
console.log(
|
||||
`libfluxcore benchmark (${workerCount} encode workers, profile=${profile}, SIMD=${process.env.FLUXCORE_WASM_SIMD === '1' ? 'on' : 'off'})`,
|
||||
);
|
||||
if (profile === 'highres') {
|
||||
await runHighResProfile(wasmBytes);
|
||||
} else if (profile === 'realmedia') {
|
||||
await runRealMediaProfile(wasmBytes);
|
||||
} else {
|
||||
await runStandardProfile(wasmBytes);
|
||||
}
|
||||
}
|
||||
@@ -161,218 +161,10 @@ function calculateStatusGeometry(avatarSize: number, isMobile: boolean = false):
|
||||
};
|
||||
}
|
||||
|
||||
function generateAvatarMaskDefault(size: number): string {
|
||||
const r = size / 2;
|
||||
return `<circle fill="white" cx="${r}" cy="${r}" r="${r}" />`;
|
||||
}
|
||||
|
||||
function generateAvatarMaskStatusRound(size: number): string {
|
||||
const r = size / 2;
|
||||
const status = calculateStatusGeometry(size);
|
||||
return `(
|
||||
<>
|
||||
<circle fill="white" cx="${r}" cy="${r}" r="${r}" />
|
||||
<circle fill="black" cx="${status.cx}" cy="${status.cy}" r="${status.outerRadius}" />
|
||||
</>
|
||||
)`;
|
||||
}
|
||||
|
||||
function generateAvatarMaskStatusTyping(size: number): string {
|
||||
const r = size / 2;
|
||||
const status = calculateStatusGeometry(size);
|
||||
const typingWidth = Math.round(status.size * TYPING_WIDTH_MULTIPLIER);
|
||||
const typingRx = status.outerRadius;
|
||||
const typingExtension = Math.max(0, typingWidth - status.size);
|
||||
const typingBridgeShift = typingExtension * TYPING_BRIDGE_RIGHT_SHIFT_RATIO;
|
||||
const leftCx = status.cx - typingExtension + typingBridgeShift;
|
||||
const rightCx = status.cx + typingBridgeShift;
|
||||
const y = status.cy - typingRx;
|
||||
const bridgeHeight = typingRx * 2;
|
||||
return `(
|
||||
<>
|
||||
<circle fill="white" cx="${r}" cy="${r}" r="${r}" />
|
||||
<circle fill="black" cx="${rightCx}" cy="${status.cy}" r="${typingRx}" />
|
||||
<rect fill="black" x="${leftCx}" y="${y}" width="${typingExtension}" height="${bridgeHeight}" />
|
||||
<circle fill="black" cx="${leftCx}" cy="${status.cy}" r="${typingRx}" />
|
||||
</>
|
||||
)`;
|
||||
}
|
||||
|
||||
function generateMobilePhoneMask(mobileStatus: MobileStatusGeometry): string {
|
||||
const displayExtraHeight = DESIGN_RULES.mobileDisplayExtraHeight;
|
||||
const displayExtraWidthPerSide = DESIGN_RULES.mobileDisplayExtraWidthPerSide;
|
||||
const screenWidth =
|
||||
mobileStatus.phoneWidth * DESIGN_RULES.mobileScreenWidth +
|
||||
displayExtraWidthPerSide * 2 -
|
||||
MOBILE_SCREEN_WIDTH_TRIM_PX;
|
||||
const screenHeight =
|
||||
mobileStatus.phoneHeight * DESIGN_RULES.mobileScreenHeight + displayExtraHeight - MOBILE_SCREEN_HEIGHT_TRIM_PX;
|
||||
const screenX = mobileStatus.phoneX + (mobileStatus.phoneWidth - screenWidth) / 2 + MOBILE_SCREEN_X_OFFSET_PX;
|
||||
const screenY =
|
||||
mobileStatus.phoneY +
|
||||
mobileStatus.phoneHeight * DESIGN_RULES.mobileScreenY -
|
||||
displayExtraHeight / 2 +
|
||||
MOBILE_SCREEN_Y_OFFSET_PX;
|
||||
const screenRx = Math.min(screenWidth, screenHeight) * 0.1;
|
||||
const wheelRadius = mobileStatus.phoneWidth * DESIGN_RULES.mobileWheelRadius;
|
||||
const wheelCx = mobileStatus.phoneX + mobileStatus.phoneWidth / 2;
|
||||
const wheelCy = mobileStatus.phoneY + mobileStatus.phoneHeight * DESIGN_RULES.mobileWheelY;
|
||||
return `(
|
||||
<>
|
||||
<rect fill="white" x="${mobileStatus.phoneX}" y="${mobileStatus.phoneY}" width="${mobileStatus.phoneWidth}" height="${mobileStatus.phoneHeight}" rx="${mobileStatus.phoneRx}" ry="${mobileStatus.phoneRx}" />
|
||||
<rect fill="black" x="${screenX}" y="${screenY}" width="${screenWidth}" height="${screenHeight}" rx="${screenRx}" ry="${screenRx}" />
|
||||
<circle fill="black" cx="${wheelCx}" cy="${wheelCy}" r="${wheelRadius}" />
|
||||
</>
|
||||
)`;
|
||||
}
|
||||
|
||||
function generateStatusOnline(size: number, isMobile: boolean = false): string {
|
||||
const status = calculateStatusGeometry(size, isMobile);
|
||||
if (!isMobile) {
|
||||
return `<circle fill="white" cx="${status.cx}" cy="${status.cy}" r="${status.outerRadius}" />`;
|
||||
}
|
||||
return generateMobilePhoneMask(status as MobileStatusGeometry);
|
||||
}
|
||||
|
||||
function generateStatusIdle(size: number, isMobile: boolean = false): string {
|
||||
const status = calculateStatusGeometry(size, isMobile);
|
||||
if (!isMobile) {
|
||||
const cutoutRadius = Math.round(status.outerRadius * DESIGN_RULES.idle.cutoutRadiusRatio);
|
||||
const cutoutOffsetDistance = Math.round(status.outerRadius * DESIGN_RULES.idle.cutoutOffsetRatio);
|
||||
const cutoutCx = status.cx - cutoutOffsetDistance;
|
||||
const cutoutCy = status.cy - cutoutOffsetDistance;
|
||||
return `(
|
||||
<>
|
||||
<circle fill="white" cx="${status.cx}" cy="${status.cy}" r="${status.outerRadius}" />
|
||||
<circle fill="black" cx="${cutoutCx}" cy="${cutoutCy}" r="${cutoutRadius}" />
|
||||
</>
|
||||
)`;
|
||||
}
|
||||
return generateMobilePhoneMask(status as MobileStatusGeometry);
|
||||
}
|
||||
|
||||
function generateStatusDnd(size: number, isMobile: boolean = false): string {
|
||||
const status = calculateStatusGeometry(size, isMobile);
|
||||
if (!isMobile) {
|
||||
const barWidth = Math.round(status.outerRadius * DESIGN_RULES.dnd.barWidthRatio);
|
||||
const rawBarHeight = status.outerRadius * DESIGN_RULES.dnd.barHeightRatio;
|
||||
const barHeight = Math.max(DESIGN_RULES.dnd.minBarHeight, Math.round(rawBarHeight));
|
||||
const barX = status.cx - barWidth / 2;
|
||||
const barY = status.cy - barHeight / 2;
|
||||
const barRx = barHeight / 2;
|
||||
return `(
|
||||
<>
|
||||
<circle fill="white" cx="${status.cx}" cy="${status.cy}" r="${status.outerRadius}" />
|
||||
<rect fill="black" x="${barX}" y="${barY}" width="${barWidth}" height="${barHeight}" rx="${barRx}" ry="${barRx}" />
|
||||
</>
|
||||
)`;
|
||||
}
|
||||
return generateMobilePhoneMask(status as MobileStatusGeometry);
|
||||
}
|
||||
|
||||
function generateStatusOffline(size: number): string {
|
||||
const status = calculateStatusGeometry(size);
|
||||
const innerRadius = Math.round(status.innerRadius * DESIGN_RULES.offline.innerRingRatio);
|
||||
return `(
|
||||
<>
|
||||
<circle fill="white" cx="${status.cx}" cy="${status.cy}" r="${status.outerRadius}" />
|
||||
<circle fill="black" cx="${status.cx}" cy="${status.cy}" r="${innerRadius}" />
|
||||
</>
|
||||
)`;
|
||||
}
|
||||
|
||||
function generateStatusTyping(size: number): string {
|
||||
const status = calculateStatusGeometry(size);
|
||||
const typingWidth = Math.round(status.size * TYPING_WIDTH_MULTIPLIER);
|
||||
const typingHeight = status.size;
|
||||
const rx = status.outerRadius;
|
||||
const typingExtension = Math.max(0, typingWidth - status.size);
|
||||
const typingBridgeShift = typingExtension * TYPING_BRIDGE_RIGHT_SHIFT_RATIO;
|
||||
const x = status.cx - status.size / 2 - typingExtension + typingBridgeShift;
|
||||
const y = status.cy - typingHeight / 2;
|
||||
return `<rect fill="white" x="${x}" y="${y}" width="${typingWidth}" height="${typingHeight}" rx="${rx}" ry="${rx}" />`;
|
||||
}
|
||||
|
||||
const SIZES: Array<AvatarSize> = [16, 20, 24, 32, 36, 40, 44, 48, 56, 80, 120];
|
||||
const GENERATED_HEADER = '// SPDX-License-Identifier: AGPL-3.0-or-later\n\n';
|
||||
|
||||
let output = `${GENERATED_HEADER}type AvatarSize = ${SIZES.join(' | ')};
|
||||
|
||||
interface MaskDefinition {
|
||||
viewBox: string;
|
||||
content: React.ReactElement;
|
||||
}
|
||||
|
||||
interface MaskSet {
|
||||
avatarDefault: MaskDefinition;
|
||||
avatarStatusRound: MaskDefinition;
|
||||
avatarStatusTyping: MaskDefinition;
|
||||
statusOnline: MaskDefinition;
|
||||
statusOnlineMobile: MaskDefinition;
|
||||
statusIdle: MaskDefinition;
|
||||
statusIdleMobile: MaskDefinition;
|
||||
statusDnd: MaskDefinition;
|
||||
statusDndMobile: MaskDefinition;
|
||||
statusOffline: MaskDefinition;
|
||||
statusTyping: MaskDefinition;
|
||||
}
|
||||
|
||||
export const AVATAR_MASKS: Record<AvatarSize, MaskSet> = {
|
||||
`;
|
||||
|
||||
for (const size of SIZES) {
|
||||
output += ` ${size}: {
|
||||
avatarDefault: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateAvatarMaskDefault(size)},
|
||||
},
|
||||
avatarStatusRound: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateAvatarMaskStatusRound(size)},
|
||||
},
|
||||
avatarStatusTyping: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateAvatarMaskStatusTyping(size)},
|
||||
},
|
||||
statusOnline: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusOnline(size, false)},
|
||||
},
|
||||
statusOnlineMobile: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusOnline(size, true)},
|
||||
},
|
||||
statusIdle: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusIdle(size, false)},
|
||||
},
|
||||
statusIdleMobile: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusIdle(size, true)},
|
||||
},
|
||||
statusDnd: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusDnd(size, false)},
|
||||
},
|
||||
statusDndMobile: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusDnd(size, true)},
|
||||
},
|
||||
statusOffline: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusOffline(size)},
|
||||
},
|
||||
statusTyping: {
|
||||
viewBox: '0 0 ${size} ${size}',
|
||||
content: ${generateStatusTyping(size)},
|
||||
},
|
||||
},
|
||||
`;
|
||||
}
|
||||
|
||||
output += `} as const;
|
||||
|
||||
export const SVGMasks = () => (
|
||||
let output = `${GENERATED_HEADER}export const SVGMasks = () => (
|
||||
<svg
|
||||
viewBox="0 0 1 1"
|
||||
aria-hidden={true}
|
||||
|
||||
@@ -5,67 +5,3 @@ import * as path from 'node:path';
|
||||
export const ROOT_DIR = path.resolve(import.meta.dirname, '..', '..');
|
||||
export const SRC_DIR = path.join(ROOT_DIR, 'src');
|
||||
export const DIST_DIR = path.join(ROOT_DIR, 'dist');
|
||||
export const ASSETS_DIR = path.join(DIST_DIR, 'assets');
|
||||
export const PKGS_DIR = path.join(ROOT_DIR, 'pkgs');
|
||||
export const PUBLIC_DIR = path.join(ROOT_DIR, 'assets');
|
||||
|
||||
export const CDN_ENDPOINT = 'https://fluxerstatic.com';
|
||||
|
||||
export const DEV_PORT = 3000;
|
||||
|
||||
export const RESOLVE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.json', '.mjs', '.cjs'];
|
||||
|
||||
export const LOCALES = [
|
||||
'ar',
|
||||
'bg',
|
||||
'cs',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'en-US',
|
||||
'es-419',
|
||||
'es-ES',
|
||||
'fi',
|
||||
'fr',
|
||||
'he',
|
||||
'hi',
|
||||
'hr',
|
||||
'hu',
|
||||
'id',
|
||||
'it',
|
||||
'ja',
|
||||
'ko',
|
||||
'lt',
|
||||
'nl',
|
||||
'no',
|
||||
'pl',
|
||||
'pt-BR',
|
||||
'ro',
|
||||
'ru',
|
||||
'sv-SE',
|
||||
'th',
|
||||
'tr',
|
||||
'uk',
|
||||
'vi',
|
||||
'zh-CN',
|
||||
'zh-TW',
|
||||
];
|
||||
|
||||
export const FILE_LOADERS: Record<string, 'file'> = {
|
||||
'.woff': 'file',
|
||||
'.woff2': 'file',
|
||||
'.ttf': 'file',
|
||||
'.eot': 'file',
|
||||
'.png': 'file',
|
||||
'.jpg': 'file',
|
||||
'.jpeg': 'file',
|
||||
'.gif': 'file',
|
||||
'.webp': 'file',
|
||||
'.ico': 'file',
|
||||
'.mp3': 'file',
|
||||
'.wav': 'file',
|
||||
'.ogg': 'file',
|
||||
'.mp4': 'file',
|
||||
'.webm': 'file',
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2023"],
|
||||
"noEmit": true,
|
||||
"moduleResolution": "Bundler",
|
||||
"moduleDetection": "force",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"jsx": "preserve",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"@app_scripts/*": ["./../*"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*.ts", "./**/*.tsx"]
|
||||
}
|
||||
Vendored
-65
@@ -1,65 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
declare module 'postcss' {
|
||||
interface ProcessOptions {
|
||||
from?: string;
|
||||
to?: string;
|
||||
map?: boolean | {inline?: boolean; prev?: boolean | string | object; annotation?: boolean | string};
|
||||
parser?: unknown;
|
||||
stringifier?: unknown;
|
||||
syntax?: unknown;
|
||||
}
|
||||
|
||||
interface Result {
|
||||
css: string;
|
||||
map?: unknown;
|
||||
root: unknown;
|
||||
processor: unknown;
|
||||
messages: Array<unknown>;
|
||||
opts: ProcessOptions;
|
||||
}
|
||||
|
||||
interface Plugin {
|
||||
postcssPlugin: string;
|
||||
Once?(root: unknown, helpers: unknown): void | Promise<void>;
|
||||
Root?(root: unknown, helpers: unknown): void | Promise<void>;
|
||||
RootExit?(root: unknown, helpers: unknown): void | Promise<void>;
|
||||
AtRule?(atRule: unknown, helpers: unknown): void | Promise<void>;
|
||||
AtRuleExit?(atRule: unknown, helpers: unknown): void | Promise<void>;
|
||||
Rule?(rule: unknown, helpers: unknown): void | Promise<void>;
|
||||
RuleExit?(rule: unknown, helpers: unknown): void | Promise<void>;
|
||||
Declaration?(declaration: unknown, helpers: unknown): void | Promise<void>;
|
||||
DeclarationExit?(declaration: unknown, helpers: unknown): void | Promise<void>;
|
||||
Comment?(comment: unknown, helpers: unknown): void | Promise<void>;
|
||||
CommentExit?(comment: unknown, helpers: unknown): void | Promise<void>;
|
||||
}
|
||||
|
||||
interface Processor {
|
||||
process(css: string, options?: ProcessOptions): Promise<Result>;
|
||||
}
|
||||
|
||||
function postcss(plugins?: Array<Plugin | ((options?: unknown) => Plugin)>): Processor;
|
||||
|
||||
export default postcss;
|
||||
}
|
||||
|
||||
declare module 'postcss-modules' {
|
||||
interface PostcssModulesOptions {
|
||||
localsConvention?:
|
||||
| 'camelCase'
|
||||
| 'camelCaseOnly'
|
||||
| 'dashes'
|
||||
| 'dashesOnly'
|
||||
| ((originalClassName: string, generatedClassName: string, inputFile: string) => string);
|
||||
generateScopedName?: string | ((name: string, filename: string, css: string) => string);
|
||||
getJSON?(cssFileName: string, json: Record<string, string>, outputFileName?: string): void;
|
||||
hashPrefix?: string;
|
||||
scopeBehaviour?: 'global' | 'local';
|
||||
globalModulePaths?: Array<RegExp>;
|
||||
root?: string;
|
||||
}
|
||||
|
||||
function postcssModules(options?: PostcssModulesOptions): import('postcss').Plugin;
|
||||
|
||||
export default postcssModules;
|
||||
}
|
||||
Reference in new issue
Block a user