@social.io/catalog 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.smartconfig.json +6 -2
- package/dist_bundle/bundle.js +262 -238
- package/dist_bundle/sio-logo-render.js +319 -0
- package/dist_bundle/sio-logo-worker.js +38 -0
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/elements/index.d.ts +1 -0
- package/dist_ts_web/elements/index.js +2 -1
- package/dist_ts_web/elements/sio-channel-rail.d.ts +1 -0
- package/dist_ts_web/elements/sio-channel-rail.js +14 -1
- package/dist_ts_web/elements/sio-logo-render.d.ts +43 -0
- package/dist_ts_web/elements/sio-logo-render.js +319 -0
- package/dist_ts_web/elements/sio-logo-worker.d.ts +1 -0
- package/dist_ts_web/elements/sio-logo-worker.js +38 -0
- package/dist_ts_web/elements/sio-logo.d.ts +85 -0
- package/dist_ts_web/elements/sio-logo.js +577 -0
- package/dist_ts_web/elements/sio-tab-bar.d.ts +2 -2
- package/dist_ts_web/elements/sio-tab-bar.js +3 -3
- package/package.json +5 -5
- package/readme.hints.md +5 -4
- package/readme.md +38 -5
- package/ts_web/00_commitinfo_data.ts +2 -2
- package/ts_web/elements/index.ts +1 -0
- package/ts_web/elements/sio-channel-rail.ts +13 -0
- package/ts_web/elements/sio-logo-render.ts +388 -0
- package/ts_web/elements/sio-logo-worker.ts +67 -0
- package/ts_web/elements/sio-logo.ts +527 -0
- package/ts_web/elements/sio-tab-bar.ts +3 -3
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@social.io/catalog",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Apple-native v8 design system and web components for social.io",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./dist_ts_web/index.js"
|
|
8
8
|
},
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@git.zone/tsbuild": "^4.4.2",
|
|
19
|
-
"@git.zone/tsbundle": "^2.
|
|
19
|
+
"@git.zone/tsbundle": "^2.11.2",
|
|
20
20
|
"@git.zone/tsrun": "^2.0.5",
|
|
21
21
|
"@git.zone/tstest": "^3.6.6",
|
|
22
|
-
"@git.zone/tswatch": "^3.3.
|
|
23
|
-
"@types/node": "^26.1.
|
|
22
|
+
"@git.zone/tswatch": "^3.3.5",
|
|
23
|
+
"@types/node": "^26.1.1",
|
|
24
24
|
"@design.estate/dees-wcctools": "^3.9.0"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
package/readme.hints.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @social.io/catalog — hints
|
|
2
2
|
|
|
3
|
-
## v2 architecture (
|
|
3
|
+
## v2 architecture (v8 design program)
|
|
4
4
|
|
|
5
5
|
- Pure web-component catalog: Lit via `@design.estate/dees-element` with
|
|
6
6
|
standard TC39 decorators (`@property({ ... }) public accessor x`,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
media-recorder stack was removed.
|
|
9
9
|
- Every element: `@customElement('sio-...')`, a
|
|
10
10
|
`declare global { interface HTMLElementTagNameMap }` entry, a
|
|
11
|
-
`public static demo = () => html\`...\`` with realistic
|
|
11
|
+
`public static demo = () => html\`...\`` with realistic v8 content, and
|
|
12
12
|
`static styles = [...sioElementStyles, css\`...\`]`.
|
|
13
13
|
- Entry points:
|
|
14
14
|
- `ts_web/index.ts` → re-exports `ts_web/elements/index.ts` (the barrel:
|
|
@@ -54,9 +54,10 @@
|
|
|
54
54
|
- Icons: `sio-icon` is a thin wrapper over the `lucide` npm package
|
|
55
55
|
(never a CDN), with a small alias map (e.g. `envelope` → `mail`).
|
|
56
56
|
|
|
57
|
-
## Component inventory (
|
|
57
|
+
## Component inventory (28 files / 29 elements)
|
|
58
58
|
|
|
59
|
-
Foundation: `sio-
|
|
59
|
+
Foundation: `sio-logo` (faceted canvas mark, theme/DPR/reduced-motion aware),
|
|
60
|
+
`sio-icon`, `sio-button`, `sio-kbd`, `sio-chip`
|
|
60
61
|
(channel/lane/status palettes), `sio-avatar` + `sio-avatar-stack`,
|
|
61
62
|
`sio-unread-badge`.
|
|
62
63
|
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @social.io/catalog
|
|
2
2
|
|
|
3
|
-
The social.io
|
|
3
|
+
The social.io v8 design system and web component library: Apple-native Lit components for the unified-inbox product family — the faceted social.io brand mark, feed rows, channel navigation, command palette, media primitives, and the embeddable chat-widget suite. Built on [@design.estate/dees-element](https://www.npmjs.com/package/@design.estate/dees-element) (Lit 3, standard TC39 decorators) with a CSS-custom-property token system and full light/dark theming.
|
|
4
4
|
|
|
5
5
|
## Issue Reporting and Security
|
|
6
6
|
|
|
@@ -14,7 +14,8 @@ pnpm add @social.io/catalog
|
|
|
14
14
|
|
|
15
15
|
## Design System
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Product UI components draw from the token layer in `tokens.ts`; `sio-logo`
|
|
18
|
+
preserves the canonical v8 facet palette as brand artwork:
|
|
18
19
|
|
|
19
20
|
- **Semantic surfaces** — `--sio-bg`, `--sio-surface`, `--sio-surface-2`, four label levels, and 0.5px hairline separators, switching between iOS-style light (`#F2F2F7`/`#FFFFFF`) and true-black dark (`#000`/`#1C1C1E`).
|
|
20
21
|
- **Tint** — `--sio-tint` `#0A84FF`, deliberately brightened to `#409CFF` in dark mode, with hover and soft (translucent) variants.
|
|
@@ -42,10 +43,19 @@ public static styles = [
|
|
|
42
43
|
|
|
43
44
|
Components auto-register as custom elements on import.
|
|
44
45
|
|
|
46
|
+
```typescript
|
|
47
|
+
import '@social.io/catalog';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<sio-logo size="30" theme="auto" no-entrance></sio-logo>
|
|
52
|
+
```
|
|
53
|
+
|
|
45
54
|
### Foundation
|
|
46
55
|
|
|
47
56
|
| Element | Purpose |
|
|
48
57
|
| --- | --- |
|
|
58
|
+
| `sio-logo` | Faceted social.io S mark with tight/square layouts, palette modes, and reduced-motion-aware animations |
|
|
49
59
|
| `sio-icon` | Lucide icon renderer (`icon`, `size`, `color`, `strokeWidth`) |
|
|
50
60
|
| `sio-button` | Primary / secondary / destructive / outline / ghost variants, sm–lg sizes, status spinner |
|
|
51
61
|
| `sio-avatar` / `sio-avatar-stack` | Initials-on-tint circles with presence dot, channel badge, overlapping stacks |
|
|
@@ -53,6 +63,24 @@ Components auto-register as custom elements on import.
|
|
|
53
63
|
| `sio-unread-badge` | Count pills in channel colors, rail-sized variant |
|
|
54
64
|
| `sio-kbd` | Keyboard-shortcut hint chip (mono, hairline border) |
|
|
55
65
|
|
|
66
|
+
#### `sio-logo`
|
|
67
|
+
|
|
68
|
+
| Property | Default | Purpose |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `size` | `40` | Mark height in CSS pixels; tight-crop width follows the brand aspect ratio |
|
|
71
|
+
| `background` | `transparent` | Canvas background color |
|
|
72
|
+
| `gap` | `2` | Inset between facets in the 500px design grid |
|
|
73
|
+
| `radius` | `16` | Facet corner radius in the 500px design grid |
|
|
74
|
+
| `square` | `false` | Render on a square tile instead of the tight S crop |
|
|
75
|
+
| `mono` | empty | Use one color while retaining facet depth |
|
|
76
|
+
| `theme` | `auto` | Select `auto`, `light`, or `dark` backdrop palette behavior |
|
|
77
|
+
| `loading` | `false` | Run the looping loading treatment |
|
|
78
|
+
| `no-entrance` | `false` | Disable the entrance animation |
|
|
79
|
+
| `no-hover` | `false` | Disable the pointer wave animation |
|
|
80
|
+
|
|
81
|
+
The public `play()`, `wave()`, and `dismiss()` methods trigger the entrance,
|
|
82
|
+
wave, and exit treatments. All motion respects `prefers-reduced-motion`.
|
|
83
|
+
|
|
56
84
|
### Chrome and navigation
|
|
57
85
|
|
|
58
86
|
| Element | Purpose |
|
|
@@ -60,9 +88,14 @@ Components auto-register as custom elements on import.
|
|
|
60
88
|
| `sio-glass-toolbar` | 44px glass bar with start/center/end slots |
|
|
61
89
|
| `sio-segmented` | iOS-style segmented control with counts |
|
|
62
90
|
| `sio-tab-bar` | Mobile bottom glass bar, channel tabs + optional calendar |
|
|
63
|
-
| `sio-channel-rail` | 64px desktop rail with per-channel unread badges |
|
|
91
|
+
| `sio-channel-rail` | 64px desktop rail with the built-in v8 brand mark and per-channel unread badges |
|
|
64
92
|
| `sio-dropdown-menu` | Slot-triggered menu with destructive/disabled items |
|
|
65
93
|
|
|
94
|
+
`sio-tab-bar` uses `max(30px, var(--sio-safe-area-bottom,
|
|
95
|
+
env(safe-area-inset-bottom, 0px)))` for its bottom padding. Set
|
|
96
|
+
`--sio-safe-area-bottom` when a screenshot or native shell supplies a known
|
|
97
|
+
system-reserved area.
|
|
98
|
+
|
|
66
99
|
### Lists and feed
|
|
67
100
|
|
|
68
101
|
| Element | Purpose |
|
|
@@ -118,7 +151,7 @@ Every component ships a `static demo` rendered in the wcctools dashboard, groupe
|
|
|
118
151
|
|
|
119
152
|
## License and Legal Information
|
|
120
153
|
|
|
121
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the
|
|
154
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.
|
|
122
155
|
|
|
123
156
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
124
157
|
|
|
@@ -130,7 +163,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
130
163
|
|
|
131
164
|
### Company Information
|
|
132
165
|
|
|
133
|
-
Task Venture Capital GmbH
|
|
166
|
+
Task Venture Capital GmbH<br>
|
|
134
167
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
135
168
|
|
|
136
169
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@social.io/catalog',
|
|
6
|
-
version: '2.
|
|
7
|
-
description: '
|
|
6
|
+
version: '2.1.1',
|
|
7
|
+
description: 'Apple-native v8 design system and web components for social.io'
|
|
8
8
|
}
|
package/ts_web/elements/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from '@design.estate/dees-element';
|
|
9
9
|
import { sioElementStyles } from './tokens.js';
|
|
10
10
|
import './sio-icon.js';
|
|
11
|
+
import './sio-logo.js';
|
|
11
12
|
|
|
12
13
|
declare global {
|
|
13
14
|
interface HTMLElementTagNameMap {
|
|
@@ -123,6 +124,15 @@ export class SioChannelRail extends DeesElement {
|
|
|
123
124
|
border-right: var(--sio-hairline) solid var(--sio-separator);
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
.brand {
|
|
128
|
+
height: 35px;
|
|
129
|
+
box-sizing: border-box;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: flex-start;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
padding: 2px 0 9px;
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
.item {
|
|
127
137
|
appearance: none;
|
|
128
138
|
border: none;
|
|
@@ -204,6 +214,9 @@ export class SioChannelRail extends DeesElement {
|
|
|
204
214
|
public render(): TemplateResult {
|
|
205
215
|
return html`
|
|
206
216
|
<nav class="rail" part="rail">
|
|
217
|
+
<div class="brand" part="brand">
|
|
218
|
+
<sio-logo size="24" no-entrance no-hover aria-hidden="true"></sio-logo>
|
|
219
|
+
</div>
|
|
207
220
|
${this.channels.map(
|
|
208
221
|
(channel) => html`
|
|
209
222
|
<button
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
export type TSioLogoAnimationPhase = 'in' | 'out' | 'wave' | 'loading';
|
|
2
|
+
|
|
3
|
+
export interface ISioLogoRenderOptions {
|
|
4
|
+
size: number;
|
|
5
|
+
dpr: number;
|
|
6
|
+
gap: number;
|
|
7
|
+
radius: number;
|
|
8
|
+
square: boolean;
|
|
9
|
+
background: string;
|
|
10
|
+
mono: string;
|
|
11
|
+
light: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TSioLogoRenderContext =
|
|
15
|
+
| CanvasRenderingContext2D
|
|
16
|
+
| OffscreenCanvasRenderingContext2D;
|
|
17
|
+
|
|
18
|
+
interface ISioLogoPoint {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ISioLogoFacet {
|
|
24
|
+
fill: string;
|
|
25
|
+
points: [[number, number], [number, number], [number, number]];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const facets: ISioLogoFacet[] = [
|
|
29
|
+
{ fill: '#4AA0F5', points: [[250, 75], [150, 133.333], [250, 191.667]] },
|
|
30
|
+
{ fill: '#0A5AC0', points: [[250, 75], [350, 133.333], [250, 191.667]] },
|
|
31
|
+
{ fill: '#DBEBFF', points: [[150, 133.333], [250, 191.667], [150, 250]] },
|
|
32
|
+
{ fill: '#A9CDF7', points: [[250, 191.667], [150, 250], [250, 308.333]] },
|
|
33
|
+
{ fill: '#1E80E6', points: [[250, 191.667], [350, 250], [250, 308.333]] },
|
|
34
|
+
{ fill: '#1466C6', points: [[350, 250], [250, 308.333], [350, 366.667]] },
|
|
35
|
+
{ fill: '#74B4F7', points: [[250, 308.333], [350, 366.667], [250, 425]] },
|
|
36
|
+
{ fill: '#1E80E6', points: [[250, 308.333], [150, 366.667], [250, 425]] },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const lightFills = [
|
|
40
|
+
'#4AA0F5', '#0A5AC0', '#8FBEF5', '#77ACEE',
|
|
41
|
+
'#1E80E6', '#1466C6', '#5FA6F2', '#1E80E6',
|
|
42
|
+
];
|
|
43
|
+
const monoOpacities = [0.78, 0.64, 0.96, 0.86, 0.72, 0.66, 0.82, 0.72];
|
|
44
|
+
const tightBounds = { x: 146, y: 72, width: 208, height: 356 };
|
|
45
|
+
const centerX = 250;
|
|
46
|
+
const centerY = 250;
|
|
47
|
+
|
|
48
|
+
export const sioLogoAspect = tightBounds.width / tightBounds.height;
|
|
49
|
+
|
|
50
|
+
const centroids = facets.map((facet) => [
|
|
51
|
+
(facet.points[0][0] + facet.points[1][0] + facet.points[2][0]) / 3,
|
|
52
|
+
(facet.points[0][1] + facet.points[1][1] + facet.points[2][1]) / 3,
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const clamp = (value: number, minimum: number, maximum: number): number =>
|
|
56
|
+
Math.max(minimum, Math.min(maximum, value));
|
|
57
|
+
|
|
58
|
+
const finiteAtLeast = (value: number, minimum: number, fallback: number): number =>
|
|
59
|
+
Number.isFinite(value) ? Math.max(minimum, value) : fallback;
|
|
60
|
+
|
|
61
|
+
const normalizeOptions = (options: ISioLogoRenderOptions): ISioLogoRenderOptions => ({
|
|
62
|
+
...options,
|
|
63
|
+
size: finiteAtLeast(options.size, 1, 40),
|
|
64
|
+
dpr: clamp(finiteAtLeast(options.dpr, 0.5, 1), 0.5, 4),
|
|
65
|
+
gap: finiteAtLeast(options.gap, 0, 2),
|
|
66
|
+
radius: finiteAtLeast(options.radius, 0, 16),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const inset = (
|
|
70
|
+
points: ISioLogoFacet['points'],
|
|
71
|
+
gap: number,
|
|
72
|
+
): ISioLogoPoint[] => {
|
|
73
|
+
if (gap <= 0) return points.map(([x, y]) => ({ x, y }));
|
|
74
|
+
const centroidX = (points[0][0] + points[1][0] + points[2][0]) / 3;
|
|
75
|
+
const centroidY = (points[0][1] + points[1][1] + points[2][1]) / 3;
|
|
76
|
+
const lines: Array<ISioLogoPoint & { dx: number; dy: number }> = [];
|
|
77
|
+
|
|
78
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
79
|
+
const start = points[index];
|
|
80
|
+
const end = points[(index + 1) % points.length];
|
|
81
|
+
let deltaX = end[0] - start[0];
|
|
82
|
+
let deltaY = end[1] - start[1];
|
|
83
|
+
const length = Math.hypot(deltaX, deltaY);
|
|
84
|
+
deltaX /= length;
|
|
85
|
+
deltaY /= length;
|
|
86
|
+
let normalX = -deltaY;
|
|
87
|
+
let normalY = deltaX;
|
|
88
|
+
const middleX = (start[0] + end[0]) / 2;
|
|
89
|
+
const middleY = (start[1] + end[1]) / 2;
|
|
90
|
+
if ((centroidX - middleX) * normalX + (centroidY - middleY) * normalY < 0) {
|
|
91
|
+
normalX = -normalX;
|
|
92
|
+
normalY = -normalY;
|
|
93
|
+
}
|
|
94
|
+
lines.push({
|
|
95
|
+
x: start[0] + normalX * gap,
|
|
96
|
+
y: start[1] + normalY * gap,
|
|
97
|
+
dx: deltaX,
|
|
98
|
+
dy: deltaY,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return lines.map((line, index) => {
|
|
103
|
+
const previous = lines[(index - 1 + lines.length) % lines.length];
|
|
104
|
+
const denominator = previous.dx * line.dy - previous.dy * line.dx;
|
|
105
|
+
const distance =
|
|
106
|
+
((line.x - previous.x) * line.dy - (line.y - previous.y) * line.dx) /
|
|
107
|
+
denominator;
|
|
108
|
+
return {
|
|
109
|
+
x: previous.x + previous.dx * distance,
|
|
110
|
+
y: previous.y + previous.dy * distance,
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const createFacetPath = (
|
|
116
|
+
points: ISioLogoFacet['points'],
|
|
117
|
+
gap: number,
|
|
118
|
+
radius: number,
|
|
119
|
+
): Path2D => {
|
|
120
|
+
const insetPoints = inset(points, gap);
|
|
121
|
+
const path = new Path2D();
|
|
122
|
+
if (radius <= 0) {
|
|
123
|
+
path.moveTo(insetPoints[0].x, insetPoints[0].y);
|
|
124
|
+
for (let index = 1; index < insetPoints.length; index += 1) {
|
|
125
|
+
path.lineTo(insetPoints[index].x, insetPoints[index].y);
|
|
126
|
+
}
|
|
127
|
+
path.closePath();
|
|
128
|
+
return path;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (let index = 0; index < insetPoints.length; index += 1) {
|
|
132
|
+
const vertex = insetPoints[index];
|
|
133
|
+
const previous = insetPoints[(index - 1 + insetPoints.length) % insetPoints.length];
|
|
134
|
+
const next = insetPoints[(index + 1) % insetPoints.length];
|
|
135
|
+
const previousDistance = Math.hypot(previous.x - vertex.x, previous.y - vertex.y);
|
|
136
|
+
const nextDistance = Math.hypot(next.x - vertex.x, next.y - vertex.y);
|
|
137
|
+
const previousRadius = Math.min(radius, previousDistance * 0.5);
|
|
138
|
+
const nextRadius = Math.min(radius, nextDistance * 0.5);
|
|
139
|
+
const entry = {
|
|
140
|
+
x: vertex.x + ((previous.x - vertex.x) / previousDistance) * previousRadius,
|
|
141
|
+
y: vertex.y + ((previous.y - vertex.y) / previousDistance) * previousRadius,
|
|
142
|
+
};
|
|
143
|
+
const exit = {
|
|
144
|
+
x: vertex.x + ((next.x - vertex.x) / nextDistance) * nextRadius,
|
|
145
|
+
y: vertex.y + ((next.y - vertex.y) / nextDistance) * nextRadius,
|
|
146
|
+
};
|
|
147
|
+
if (index === 0) path.moveTo(entry.x, entry.y);
|
|
148
|
+
else path.lineTo(entry.x, entry.y);
|
|
149
|
+
path.quadraticCurveTo(vertex.x, vertex.y, exit.x, exit.y);
|
|
150
|
+
}
|
|
151
|
+
path.closePath();
|
|
152
|
+
return path;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const easeOutCubic = (progress: number): number => 1 - Math.pow(1 - progress, 3);
|
|
156
|
+
const easeOutBack = (progress: number): number => {
|
|
157
|
+
const coefficient = 1.70158;
|
|
158
|
+
return 1 + (coefficient + 1) * Math.pow(progress - 1, 3) + coefficient * Math.pow(progress - 1, 2);
|
|
159
|
+
};
|
|
160
|
+
const easeInCubic = (progress: number): number => progress * progress * progress;
|
|
161
|
+
const easeInBack = (progress: number): number => {
|
|
162
|
+
const coefficient = 1.70158;
|
|
163
|
+
return (coefficient + 1) * progress * progress * progress - coefficient * progress * progress;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type TSioLogoFrameHandle = number | ReturnType<typeof setTimeout>;
|
|
167
|
+
|
|
168
|
+
const requestFrame = (callback: FrameRequestCallback): TSioLogoFrameHandle => {
|
|
169
|
+
if (typeof globalThis.requestAnimationFrame === 'function') {
|
|
170
|
+
return globalThis.requestAnimationFrame(callback);
|
|
171
|
+
}
|
|
172
|
+
return setTimeout(() => callback(performance.now()), 1000 / 60);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const cancelFrame = (handle: TSioLogoFrameHandle): void => {
|
|
176
|
+
if (typeof globalThis.cancelAnimationFrame === 'function' && typeof handle === 'number') {
|
|
177
|
+
globalThis.cancelAnimationFrame(handle);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
clearTimeout(handle);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export class SioLogoRenderer {
|
|
184
|
+
private options: ISioLogoRenderOptions;
|
|
185
|
+
private frameHandle: TSioLogoFrameHandle | null = null;
|
|
186
|
+
private phase: TSioLogoAnimationPhase | null = null;
|
|
187
|
+
private startedAt = 0;
|
|
188
|
+
private stagger = 70;
|
|
189
|
+
private duration = 560;
|
|
190
|
+
private totalDuration = 0;
|
|
191
|
+
private backingWidth = 0;
|
|
192
|
+
private backingHeight = 0;
|
|
193
|
+
private scale = 1;
|
|
194
|
+
private offsetX = 0;
|
|
195
|
+
private offsetY = 0;
|
|
196
|
+
private paths: Path2D[] = [];
|
|
197
|
+
private loadingPaths: Path2D[] = [];
|
|
198
|
+
private fills: string[] = [];
|
|
199
|
+
private opacities: number[] = [];
|
|
200
|
+
|
|
201
|
+
constructor(
|
|
202
|
+
private readonly context: TSioLogoRenderContext,
|
|
203
|
+
options: ISioLogoRenderOptions,
|
|
204
|
+
) {
|
|
205
|
+
this.options = normalizeOptions(options);
|
|
206
|
+
this.rebuild();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public setOptions(options: ISioLogoRenderOptions): void {
|
|
210
|
+
this.options = normalizeOptions(options);
|
|
211
|
+
this.rebuild();
|
|
212
|
+
if (!this.phase) this.drawStatic();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public drawStatic(): void {
|
|
216
|
+
this.clearCanvas();
|
|
217
|
+
for (let index = 0; index < facets.length; index += 1) {
|
|
218
|
+
this.paint(index, 0, 0, 0, 1, this.opacities[index], this.paths[index]);
|
|
219
|
+
}
|
|
220
|
+
this.context.globalAlpha = 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public clear(): void {
|
|
224
|
+
this.stop();
|
|
225
|
+
this.clearCanvas();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
public start(phase: TSioLogoAnimationPhase): void {
|
|
229
|
+
this.stop();
|
|
230
|
+
this.phase = phase;
|
|
231
|
+
this.startedAt = performance.now();
|
|
232
|
+
this.stagger = phase === 'wave' ? 55 : 70;
|
|
233
|
+
this.duration = phase === 'wave' ? 420 : 560;
|
|
234
|
+
this.totalDuration = this.stagger * (facets.length - 1) + this.duration;
|
|
235
|
+
|
|
236
|
+
const drawFrame = (time: number): void => {
|
|
237
|
+
if (!this.phase) return;
|
|
238
|
+
const elapsed = time - this.startedAt;
|
|
239
|
+
this.drawAnimationFrame(elapsed);
|
|
240
|
+
if (this.phase !== 'loading' && elapsed >= this.totalDuration) {
|
|
241
|
+
this.settle();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
this.frameHandle = requestFrame(drawFrame);
|
|
245
|
+
};
|
|
246
|
+
this.frameHandle = requestFrame(drawFrame);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
public stop(): void {
|
|
250
|
+
if (this.frameHandle !== null) {
|
|
251
|
+
cancelFrame(this.frameHandle);
|
|
252
|
+
this.frameHandle = null;
|
|
253
|
+
}
|
|
254
|
+
this.phase = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private rebuild(): void {
|
|
258
|
+
const { size, dpr, square, light, mono, gap, radius } = this.options;
|
|
259
|
+
this.backingWidth = Math.round((square ? size : size * sioLogoAspect) * dpr);
|
|
260
|
+
this.backingHeight = Math.round(size * dpr);
|
|
261
|
+
this.context.canvas.width = this.backingWidth;
|
|
262
|
+
this.context.canvas.height = this.backingHeight;
|
|
263
|
+
if (square) {
|
|
264
|
+
this.scale = this.backingWidth / 500;
|
|
265
|
+
this.offsetX = 0;
|
|
266
|
+
this.offsetY = 0;
|
|
267
|
+
} else {
|
|
268
|
+
this.scale = this.backingHeight / tightBounds.height;
|
|
269
|
+
this.offsetX =
|
|
270
|
+
-tightBounds.x * this.scale +
|
|
271
|
+
(this.backingWidth - tightBounds.width * this.scale) / 2;
|
|
272
|
+
this.offsetY = -tightBounds.y * this.scale;
|
|
273
|
+
}
|
|
274
|
+
this.paths = facets.map((facet) => createFacetPath(facet.points, gap, radius));
|
|
275
|
+
this.loadingPaths = facets.map((facet) => createFacetPath(facet.points, gap + 3, 90));
|
|
276
|
+
this.fills = facets.map((facet, index) => mono || (light ? lightFills[index] : facet.fill));
|
|
277
|
+
this.opacities = facets.map((_, index) => (mono ? monoOpacities[index] : 1));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private clearCanvas(): void {
|
|
281
|
+
this.context.setTransform(1, 0, 0, 1, 0, 0);
|
|
282
|
+
this.context.clearRect(0, 0, this.backingWidth, this.backingHeight);
|
|
283
|
+
const { background } = this.options;
|
|
284
|
+
if (background && background !== 'transparent' && background !== 'none') {
|
|
285
|
+
this.context.fillStyle = background;
|
|
286
|
+
this.context.fillRect(0, 0, this.backingWidth, this.backingHeight);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private paint(
|
|
291
|
+
index: number,
|
|
292
|
+
translateX: number,
|
|
293
|
+
translateY: number,
|
|
294
|
+
rotation: number,
|
|
295
|
+
scale: number,
|
|
296
|
+
opacity: number,
|
|
297
|
+
path: Path2D,
|
|
298
|
+
): void {
|
|
299
|
+
const [centroidX, centroidY] = centroids[index];
|
|
300
|
+
this.context.setTransform(this.scale, 0, 0, this.scale, this.offsetX, this.offsetY);
|
|
301
|
+
this.context.translate(translateX, translateY);
|
|
302
|
+
this.context.translate(centroidX, centroidY);
|
|
303
|
+
this.context.rotate((rotation * Math.PI) / 180);
|
|
304
|
+
this.context.scale(scale, scale);
|
|
305
|
+
this.context.translate(-centroidX, -centroidY);
|
|
306
|
+
this.context.globalAlpha = opacity;
|
|
307
|
+
this.context.fillStyle = this.fills[index];
|
|
308
|
+
this.context.fill(path);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private drawAnimationFrame(elapsed: number): void {
|
|
312
|
+
this.clearCanvas();
|
|
313
|
+
const activePhase = this.phase;
|
|
314
|
+
for (let index = 0; index < facets.length; index += 1) {
|
|
315
|
+
const [centroidX, centroidY] = centroids[index];
|
|
316
|
+
let translateX = 0;
|
|
317
|
+
let translateY = 0;
|
|
318
|
+
let rotation = 0;
|
|
319
|
+
let scale = 1;
|
|
320
|
+
let opacity = this.opacities[index];
|
|
321
|
+
let path = this.paths[index];
|
|
322
|
+
|
|
323
|
+
if (activePhase === 'in') {
|
|
324
|
+
const progress = clamp((elapsed - index * this.stagger) / this.duration, 0, 1);
|
|
325
|
+
const eased = easeOutCubic(progress);
|
|
326
|
+
translateX = (centerX - centroidX) * (1 - eased);
|
|
327
|
+
translateY = (centerY - centroidY) * (1 - eased);
|
|
328
|
+
rotation = (1 - eased) * -16;
|
|
329
|
+
scale = easeOutBack(progress);
|
|
330
|
+
opacity = clamp(progress * 1.6, 0, 1) * this.opacities[index];
|
|
331
|
+
} else if (activePhase === 'out') {
|
|
332
|
+
const reverseIndex = facets.length - 1 - index;
|
|
333
|
+
const progress = clamp((elapsed - reverseIndex * this.stagger) / this.duration, 0, 1);
|
|
334
|
+
const eased = easeInCubic(progress);
|
|
335
|
+
translateX = (centerX - centroidX) * eased;
|
|
336
|
+
translateY = (centerY - centroidY) * eased;
|
|
337
|
+
rotation = progress * 16;
|
|
338
|
+
scale = Math.max(0, 1 - easeInBack(progress));
|
|
339
|
+
opacity = clamp(1 - progress * 1.3, 0, 1) * this.opacities[index];
|
|
340
|
+
} else if (activePhase === 'wave') {
|
|
341
|
+
const progress = clamp((elapsed - index * this.stagger) / this.duration, 0, 1);
|
|
342
|
+
const pulse = Math.sin(progress * Math.PI);
|
|
343
|
+
const radialX = centroidX - centerX;
|
|
344
|
+
const radialY = centroidY - centerY;
|
|
345
|
+
const radialLength = Math.hypot(radialX, radialY) || 1;
|
|
346
|
+
translateX = (radialX / radialLength) * 16 * pulse;
|
|
347
|
+
translateY = (radialY / radialLength) * 16 * pulse;
|
|
348
|
+
scale = 1 - 0.08 * pulse;
|
|
349
|
+
} else if (activePhase === 'loading') {
|
|
350
|
+
const growth = clamp(elapsed / 650, 0, 1);
|
|
351
|
+
const easedGrowth = growth < 0.5
|
|
352
|
+
? 2 * growth * growth
|
|
353
|
+
: 1 - Math.pow(-2 * growth + 2, 2) / 2;
|
|
354
|
+
const looping = elapsed >= 650;
|
|
355
|
+
const spin = (elapsed / 1500) * 2 * Math.PI;
|
|
356
|
+
const angle = -Math.PI / 2 + index * Math.PI / 4 + spin;
|
|
357
|
+
const destinationX = centerX + 120 * Math.cos(angle);
|
|
358
|
+
const destinationY = centerY + 120 * Math.sin(angle);
|
|
359
|
+
const brightness = 1 - index / facets.length;
|
|
360
|
+
translateX = (destinationX - centroidX) * easedGrowth;
|
|
361
|
+
translateY = (destinationY - centroidY) * easedGrowth;
|
|
362
|
+
scale =
|
|
363
|
+
(1 + (0.62 - 1) * easedGrowth) *
|
|
364
|
+
(looping ? 0.72 + 0.28 * brightness : 1);
|
|
365
|
+
opacity = (looping ? 0.28 + 0.72 * brightness : 1) * this.opacities[index];
|
|
366
|
+
path = looping
|
|
367
|
+
? this.loadingPaths[index]
|
|
368
|
+
: createFacetPath(
|
|
369
|
+
facets[index].points,
|
|
370
|
+
this.options.gap + 3 * easedGrowth,
|
|
371
|
+
this.options.radius + (90 - this.options.radius) * easedGrowth,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.paint(index, translateX, translateY, rotation, scale, opacity, path);
|
|
376
|
+
}
|
|
377
|
+
this.context.globalAlpha = 1;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private settle(): void {
|
|
381
|
+
const finishedPhase = this.phase;
|
|
382
|
+
this.frameHandle = null;
|
|
383
|
+
this.phase = null;
|
|
384
|
+
if (finishedPhase === 'out') this.clearCanvas();
|
|
385
|
+
else this.drawStatic();
|
|
386
|
+
this.context.globalAlpha = 1;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SioLogoRenderer,
|
|
3
|
+
type ISioLogoRenderOptions,
|
|
4
|
+
type TSioLogoAnimationPhase,
|
|
5
|
+
} from './sio-logo-render.js';
|
|
6
|
+
|
|
7
|
+
interface ISioLogoWorkerInitMessage {
|
|
8
|
+
type: 'init';
|
|
9
|
+
canvas: OffscreenCanvas;
|
|
10
|
+
options: ISioLogoRenderOptions;
|
|
11
|
+
phase: TSioLogoAnimationPhase | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ISioLogoWorkerOptionsMessage {
|
|
15
|
+
type: 'options';
|
|
16
|
+
options: ISioLogoRenderOptions;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ISioLogoWorkerPhaseMessage {
|
|
20
|
+
type: 'phase';
|
|
21
|
+
phase: TSioLogoAnimationPhase | 'stop' | 'clear';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type TSioLogoWorkerMessage =
|
|
25
|
+
| ISioLogoWorkerInitMessage
|
|
26
|
+
| ISioLogoWorkerOptionsMessage
|
|
27
|
+
| ISioLogoWorkerPhaseMessage;
|
|
28
|
+
|
|
29
|
+
interface ISioLogoWorkerScope {
|
|
30
|
+
onmessage: ((event: MessageEvent<TSioLogoWorkerMessage>) => void) | null;
|
|
31
|
+
postMessage: (message: { type: 'ready' | 'initialized' }) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const workerScope = globalThis as unknown as ISioLogoWorkerScope;
|
|
35
|
+
let renderer: SioLogoRenderer | null = null;
|
|
36
|
+
|
|
37
|
+
workerScope.onmessage = (event): void => {
|
|
38
|
+
const message = event.data;
|
|
39
|
+
if (message.type === 'init') {
|
|
40
|
+
const context = message.canvas.getContext('2d');
|
|
41
|
+
if (!context) throw new Error('Unable to create the sio-logo worker canvas context.');
|
|
42
|
+
renderer = new SioLogoRenderer(context, message.options);
|
|
43
|
+
if (message.phase) renderer.start(message.phase);
|
|
44
|
+
else renderer.drawStatic();
|
|
45
|
+
workerScope.postMessage({ type: 'initialized' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!renderer) return;
|
|
50
|
+
if (message.type === 'options') {
|
|
51
|
+
renderer.setOptions(message.options);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (message.phase === 'stop') {
|
|
56
|
+
renderer.stop();
|
|
57
|
+
renderer.drawStatic();
|
|
58
|
+
} else if (message.phase === 'clear') {
|
|
59
|
+
renderer.clear();
|
|
60
|
+
} else {
|
|
61
|
+
renderer.start(message.phase);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// The canvas is transferred only after the module and its renderer dependency
|
|
66
|
+
// have loaded, leaving the HTMLCanvasElement intact for pre-transfer fallback.
|
|
67
|
+
workerScope.postMessage({ type: 'ready' });
|