@dickpy/dsh-imagegen 1.5.8 → 1.5.10
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/README.md +1 -1
- package/lib/client.js +3512 -917
- package/lib/client.js.map +1 -1
- package/lib/index.js +18 -3
- package/package.json +1 -1
- package/src/canvas-store.ts +389 -376
- package/src/client/CanvasBackgrounds.tsx +1774 -0
- package/src/client/CanvasWorkspace.tsx +2218 -2211
- package/src/client/GooeyNav.module.css +224 -0
- package/src/client/GooeyNav.tsx +185 -0
- package/src/client/ImageGenPanel.tsx +203 -100
- package/src/client/canvas-workspace.module.css +1226 -1189
- package/src/client/locales.ts +1578 -1524
- package/src/client/panel.module.css +86 -11
- package/src/client/surface-theme.ts +20 -0
- package/src/protocol.ts +8 -5
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/* reactbits.dev "GooeyNav" port, theme-following. The gooey merge uses an SVG
|
|
2
|
+
* alpha-contrast filter (blur + feColorMatrix) instead of the CSS
|
|
3
|
+
* blur+contrast trick, so colored particles survive on any surface: the pill
|
|
4
|
+
* and ink follow the host theme via [data-theme]. */
|
|
5
|
+
|
|
6
|
+
.container {
|
|
7
|
+
--gooey-time: 1500ms;
|
|
8
|
+
--gooey-color-1: #5227ff;
|
|
9
|
+
--gooey-color-2: #ff9ffc;
|
|
10
|
+
--gooey-color-3: #b19eef;
|
|
11
|
+
--gooey-color-4: #22d3ee;
|
|
12
|
+
/* light surface (default): dark ink pill, white text */
|
|
13
|
+
--gooey-pill: #17171c;
|
|
14
|
+
--gooey-pill-ink: #ffffff;
|
|
15
|
+
--gooey-ink: #5b5e6b;
|
|
16
|
+
position: relative;
|
|
17
|
+
font-size: 13px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.container[data-theme='dark'] {
|
|
21
|
+
--gooey-pill: #ffffff;
|
|
22
|
+
--gooey-pill-ink: #17171c;
|
|
23
|
+
--gooey-ink: #ffffff;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.gooSvg {
|
|
27
|
+
position: absolute;
|
|
28
|
+
width: 0;
|
|
29
|
+
height: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.container nav {
|
|
33
|
+
display: flex;
|
|
34
|
+
position: relative;
|
|
35
|
+
transform: translate3d(0, 0, 0.01px);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.container nav ul {
|
|
39
|
+
display: flex;
|
|
40
|
+
gap: 6px;
|
|
41
|
+
list-style: none;
|
|
42
|
+
padding: 0 4px;
|
|
43
|
+
margin: 0;
|
|
44
|
+
position: relative;
|
|
45
|
+
z-index: 3;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.container nav li {
|
|
49
|
+
border-radius: 100vw;
|
|
50
|
+
position: relative;
|
|
51
|
+
transition:
|
|
52
|
+
background-color 0.3s ease,
|
|
53
|
+
color 0.3s ease,
|
|
54
|
+
box-shadow 0.3s ease;
|
|
55
|
+
color: var(--gooey-ink);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.container nav li button {
|
|
59
|
+
display: inline-block;
|
|
60
|
+
padding: 5px 13px;
|
|
61
|
+
font: inherit;
|
|
62
|
+
color: inherit;
|
|
63
|
+
background: none;
|
|
64
|
+
border: none;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
white-space: nowrap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.container nav li.active {
|
|
70
|
+
color: var(--gooey-pill-ink);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.container nav li::after {
|
|
74
|
+
content: '';
|
|
75
|
+
position: absolute;
|
|
76
|
+
inset: 0;
|
|
77
|
+
border-radius: 10px;
|
|
78
|
+
background: var(--gooey-pill);
|
|
79
|
+
opacity: 0;
|
|
80
|
+
transform: scale(0);
|
|
81
|
+
transition: all 0.3s ease;
|
|
82
|
+
z-index: -1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.container nav li.active::after {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
transform: scale(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.effect {
|
|
91
|
+
position: absolute;
|
|
92
|
+
left: 0;
|
|
93
|
+
top: 0;
|
|
94
|
+
width: 0;
|
|
95
|
+
height: 0;
|
|
96
|
+
opacity: 1;
|
|
97
|
+
pointer-events: none;
|
|
98
|
+
display: grid;
|
|
99
|
+
place-items: center;
|
|
100
|
+
z-index: 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.effectText {
|
|
104
|
+
color: var(--gooey-ink);
|
|
105
|
+
transition: color 0.3s ease;
|
|
106
|
+
font-size: 13px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.effectText.active {
|
|
110
|
+
color: var(--gooey-pill-ink);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.effectFilter {
|
|
114
|
+
filter: url(#dsh-gooey-nav-goo);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.effectFilter::after {
|
|
118
|
+
content: '';
|
|
119
|
+
position: absolute;
|
|
120
|
+
inset: 0;
|
|
121
|
+
background: var(--gooey-pill);
|
|
122
|
+
transform: scale(0);
|
|
123
|
+
opacity: 0;
|
|
124
|
+
z-index: -1;
|
|
125
|
+
border-radius: 100vw;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.effectFilter.active::after {
|
|
129
|
+
animation: gooeyPill 0.3s ease both;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@keyframes gooeyPill {
|
|
133
|
+
to {
|
|
134
|
+
transform: scale(1);
|
|
135
|
+
opacity: 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.particle,
|
|
140
|
+
.point {
|
|
141
|
+
display: block;
|
|
142
|
+
opacity: 0;
|
|
143
|
+
width: 14px;
|
|
144
|
+
height: 14px;
|
|
145
|
+
border-radius: 100%;
|
|
146
|
+
transform-origin: center;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.particle {
|
|
150
|
+
--time: 1500ms;
|
|
151
|
+
position: absolute;
|
|
152
|
+
top: calc(50% - 7px);
|
|
153
|
+
left: calc(50% - 7px);
|
|
154
|
+
animation: gooeyParticle calc(var(--time)) ease 1 -350ms;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.point {
|
|
158
|
+
background: var(--color);
|
|
159
|
+
opacity: 1;
|
|
160
|
+
animation: gooeyPoint calc(var(--time)) ease 1 -350ms;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
@keyframes gooeyParticle {
|
|
164
|
+
0% {
|
|
165
|
+
transform: rotate(0deg) translate(calc(var(--start-x)), calc(var(--start-y)));
|
|
166
|
+
opacity: 1;
|
|
167
|
+
animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
70% {
|
|
171
|
+
transform: rotate(calc(var(--rotate) * 0.5)) translate(calc(var(--end-x) * 1.2), calc(var(--end-y) * 1.2));
|
|
172
|
+
opacity: 1;
|
|
173
|
+
animation-timing-function: ease;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
85% {
|
|
177
|
+
transform: rotate(calc(var(--rotate) * 0.66)) translate(calc(var(--end-x)), calc(var(--end-y)));
|
|
178
|
+
opacity: 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
100% {
|
|
182
|
+
transform: rotate(calc(var(--rotate) * 1.2)) translate(calc(var(--end-x) * 0.5), calc(var(--end-y) * 0.5));
|
|
183
|
+
opacity: 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@keyframes gooeyPoint {
|
|
188
|
+
0% {
|
|
189
|
+
transform: scale(0);
|
|
190
|
+
opacity: 0;
|
|
191
|
+
animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
25% {
|
|
195
|
+
transform: scale(calc(var(--scale) * 0.25));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
38% {
|
|
199
|
+
opacity: 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
65% {
|
|
203
|
+
transform: scale(var(--scale));
|
|
204
|
+
opacity: 1;
|
|
205
|
+
animation-timing-function: ease;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
85% {
|
|
209
|
+
transform: scale(var(--scale));
|
|
210
|
+
opacity: 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
100% {
|
|
214
|
+
transform: scale(0);
|
|
215
|
+
opacity: 0;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@media (prefers-reduced-motion: reduce) {
|
|
220
|
+
.particle,
|
|
221
|
+
.point {
|
|
222
|
+
animation: none;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/** reactbits.dev "GooeyNav" port (controlled, dependency-free): the active
|
|
2
|
+
* pill merges with a radial burst of gooey particles through a
|
|
3
|
+
* blur+contrast filter. The original renders white ink on dark surfaces,
|
|
4
|
+
* so the palette flips per detected surface to stay readable in the host
|
|
5
|
+
* theme. */
|
|
6
|
+
|
|
7
|
+
import { useEffect, useRef, type ReactNode } from 'react'
|
|
8
|
+
import { detectLightSurface } from './surface-theme.ts'
|
|
9
|
+
import css from './GooeyNav.module.css'
|
|
10
|
+
|
|
11
|
+
export interface GooeyNavItem {
|
|
12
|
+
key: string
|
|
13
|
+
label: ReactNode
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ANIMATION_TIME = 600
|
|
17
|
+
const PARTICLE_COUNT = 15
|
|
18
|
+
const PARTICLE_DISTANCES = [90, 10]
|
|
19
|
+
const PARTICLE_R = 100
|
|
20
|
+
const TIME_VARIANCE = 300
|
|
21
|
+
const COLORS = [1, 2, 3, 1, 2, 3, 1, 4]
|
|
22
|
+
|
|
23
|
+
export function GooeyNav(props: {
|
|
24
|
+
items: GooeyNavItem[]
|
|
25
|
+
activeIndex: number
|
|
26
|
+
onSelect: (index: number) => void
|
|
27
|
+
ariaLabel?: string
|
|
28
|
+
}): React.JSX.Element {
|
|
29
|
+
const { items, activeIndex, onSelect, ariaLabel } = props
|
|
30
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
31
|
+
const navRef = useRef<HTMLUListElement>(null)
|
|
32
|
+
const filterRef = useRef<HTMLSpanElement>(null)
|
|
33
|
+
const textRef = useRef<HTMLSpanElement>(null)
|
|
34
|
+
const burstRef = useRef<(li: HTMLElement) => void>(() => {})
|
|
35
|
+
const timersRef = useRef<number[]>([])
|
|
36
|
+
const activeIndexRef = useRef(activeIndex)
|
|
37
|
+
activeIndexRef.current = activeIndex
|
|
38
|
+
|
|
39
|
+
const updateEffectPosition = (element: HTMLElement): void => {
|
|
40
|
+
const container = containerRef.current
|
|
41
|
+
const filter = filterRef.current
|
|
42
|
+
const text = textRef.current
|
|
43
|
+
if (container === null || filter === null || text === null) return
|
|
44
|
+
const containerRect = container.getBoundingClientRect()
|
|
45
|
+
const pos = element.getBoundingClientRect()
|
|
46
|
+
const styles = {
|
|
47
|
+
left: `${pos.x - containerRect.x}px`,
|
|
48
|
+
top: `${pos.y - containerRect.y}px`,
|
|
49
|
+
width: `${pos.width}px`,
|
|
50
|
+
height: `${pos.height}px`,
|
|
51
|
+
}
|
|
52
|
+
Object.assign(filter.style, styles)
|
|
53
|
+
Object.assign(text.style, styles)
|
|
54
|
+
text.textContent = element.textContent
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// One-time setup: the particle burst timers must survive selection changes,
|
|
58
|
+
// so they live outside the activeIndex-driven effect below.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
const container = containerRef.current
|
|
61
|
+
const filter = filterRef.current
|
|
62
|
+
const text = textRef.current
|
|
63
|
+
if (container === null || filter === null || text === null) return
|
|
64
|
+
container.dataset.theme = detectLightSurface(container) ? 'light' : 'dark'
|
|
65
|
+
let disposed = false
|
|
66
|
+
const noise = (amount: number): number => amount / 2 - Math.random() * amount
|
|
67
|
+
const getXY = (distance: number, pointIndex: number, totalPoints: number): [number, number] => {
|
|
68
|
+
const angle = ((360 + noise(8)) / totalPoints) * pointIndex * (Math.PI / 180)
|
|
69
|
+
return [distance * Math.cos(angle), distance * Math.sin(angle)]
|
|
70
|
+
}
|
|
71
|
+
const makeParticles = (element: HTMLElement): void => {
|
|
72
|
+
const bubbleTime = ANIMATION_TIME * 2 + TIME_VARIANCE
|
|
73
|
+
element.style.setProperty('--time', `${bubbleTime}ms`)
|
|
74
|
+
for (let i = 0; i < PARTICLE_COUNT; i++) {
|
|
75
|
+
const time = ANIMATION_TIME * 2 + noise(TIME_VARIANCE * 2)
|
|
76
|
+
const rotateSeed = noise(PARTICLE_R / 10)
|
|
77
|
+
const start = getXY(PARTICLE_DISTANCES[0] as number, PARTICLE_COUNT - i, PARTICLE_COUNT)
|
|
78
|
+
const end = getXY((PARTICLE_DISTANCES[1] as number) + noise(7), PARTICLE_COUNT - i, PARTICLE_COUNT)
|
|
79
|
+
const scale = 1 + noise(0.2)
|
|
80
|
+
const color = COLORS[Math.floor(Math.random() * COLORS.length)] ?? 1
|
|
81
|
+
const rotate = rotateSeed > 0 ? (rotateSeed + PARTICLE_R / 20) * 10 : (rotateSeed - PARTICLE_R / 20) * 10
|
|
82
|
+
const timer = window.setTimeout(() => {
|
|
83
|
+
if (disposed) return
|
|
84
|
+
const particle = document.createElement('span')
|
|
85
|
+
const point = document.createElement('span')
|
|
86
|
+
particle.className = css.particle
|
|
87
|
+
point.className = css.point
|
|
88
|
+
particle.style.setProperty('--start-x', `${start[0] as number}px`)
|
|
89
|
+
particle.style.setProperty('--start-y', `${start[1] as number}px`)
|
|
90
|
+
particle.style.setProperty('--end-x', `${end[0] as number}px`)
|
|
91
|
+
particle.style.setProperty('--end-y', `${end[1] as number}px`)
|
|
92
|
+
particle.style.setProperty('--time', `${time}ms`)
|
|
93
|
+
particle.style.setProperty('--scale', `${scale}`)
|
|
94
|
+
particle.style.setProperty('--color', `var(--gooey-color-${color}, currentColor)`)
|
|
95
|
+
particle.style.setProperty('--rotate', `${rotate}deg`)
|
|
96
|
+
particle.appendChild(point)
|
|
97
|
+
element.appendChild(particle)
|
|
98
|
+
if (typeof window.requestAnimationFrame === 'function') {
|
|
99
|
+
window.requestAnimationFrame(() => { element.classList.add(css.active) })
|
|
100
|
+
} else {
|
|
101
|
+
element.classList.add(css.active)
|
|
102
|
+
}
|
|
103
|
+
const removeTimer = window.setTimeout(() => { particle.remove() }, time)
|
|
104
|
+
timersRef.current.push(removeTimer)
|
|
105
|
+
}, 30)
|
|
106
|
+
timersRef.current.push(timer)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
burstRef.current = (li: HTMLElement): void => {
|
|
110
|
+
updateEffectPosition(li)
|
|
111
|
+
for (const particle of [...filter.querySelectorAll(`.${css.particle}`)]) particle.remove()
|
|
112
|
+
text.classList.remove(css.active)
|
|
113
|
+
void text.offsetWidth
|
|
114
|
+
text.classList.add(css.active)
|
|
115
|
+
makeParticles(filter)
|
|
116
|
+
}
|
|
117
|
+
const positionActive = (): void => {
|
|
118
|
+
const nav = navRef.current
|
|
119
|
+
const activeLi = nav?.querySelectorAll('li')[activeIndexRef.current]
|
|
120
|
+
if (activeLi) {
|
|
121
|
+
updateEffectPosition(activeLi)
|
|
122
|
+
text.classList.add(css.active)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
positionActive()
|
|
126
|
+
const observer = new ResizeObserver(positionActive)
|
|
127
|
+
observer.observe(container)
|
|
128
|
+
return () => {
|
|
129
|
+
disposed = true
|
|
130
|
+
for (const timer of timersRef.current) window.clearTimeout(timer)
|
|
131
|
+
timersRef.current = []
|
|
132
|
+
observer.disconnect()
|
|
133
|
+
}
|
|
134
|
+
}, [])
|
|
135
|
+
|
|
136
|
+
// External selection changes only reposition the pill (no burst); clicks
|
|
137
|
+
// animate through burstRef above.
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
const nav = navRef.current
|
|
140
|
+
const text = textRef.current
|
|
141
|
+
const activeLi = nav?.querySelectorAll('li')[activeIndex]
|
|
142
|
+
if (activeLi) {
|
|
143
|
+
updateEffectPosition(activeLi)
|
|
144
|
+
text?.classList.add(css.active)
|
|
145
|
+
}
|
|
146
|
+
}, [activeIndex])
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className={css.container} ref={containerRef}>
|
|
150
|
+
<svg className={css.gooSvg} aria-hidden="true" focusable="false">
|
|
151
|
+
<defs>
|
|
152
|
+
{/* Alpha-contrast goo: merges the pill and particles without
|
|
153
|
+
crushing their colors the way CSS contrast() would. */}
|
|
154
|
+
<filter id="dsh-gooey-nav-goo" x="-150%" y="-150%" width="400%" height="400%">
|
|
155
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
|
|
156
|
+
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 19 -9" />
|
|
157
|
+
</filter>
|
|
158
|
+
</defs>
|
|
159
|
+
</svg>
|
|
160
|
+
<nav aria-label={ariaLabel} role="tablist">
|
|
161
|
+
<ul ref={navRef}>
|
|
162
|
+
{items.map((item, index) => (
|
|
163
|
+
<li key={item.key} className={activeIndex === index ? css.active : undefined}>
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
role="tab"
|
|
167
|
+
aria-selected={activeIndex === index}
|
|
168
|
+
onClick={event => {
|
|
169
|
+
if (index === activeIndex) return
|
|
170
|
+
const li = event.currentTarget.closest('li')
|
|
171
|
+
if (li !== null) burstRef.current(li)
|
|
172
|
+
onSelect(index)
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
{item.label}
|
|
176
|
+
</button>
|
|
177
|
+
</li>
|
|
178
|
+
))}
|
|
179
|
+
</ul>
|
|
180
|
+
</nav>
|
|
181
|
+
<span className={`${css.effect} ${css.effectFilter}`} ref={filterRef} />
|
|
182
|
+
<span className={`${css.effect} ${css.effectText}`} ref={textRef} />
|
|
183
|
+
</div>
|
|
184
|
+
)
|
|
185
|
+
}
|