lgimgui 0.1.0
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 +111 -0
- package/demo/public/materials.json +809 -0
- package/package.json +39 -0
- package/src/core/curve.js +71 -0
- package/src/core/frame.js +272 -0
- package/src/core/geometry.js +61 -0
- package/src/core/ids.js +31 -0
- package/src/core/input.js +294 -0
- package/src/core/layout.js +220 -0
- package/src/core/spring.js +113 -0
- package/src/core/state.js +54 -0
- package/src/core/theme.js +77 -0
- package/src/index.js +221 -0
- package/src/material/adapt.js +72 -0
- package/src/material/material.js +131 -0
- package/src/material/optics.js +49 -0
- package/src/material/params.js +83 -0
- package/src/material/presets.js +291 -0
- package/src/render/batch.js +236 -0
- package/src/render/context.js +138 -0
- package/src/render/glyphs.js +177 -0
- package/src/render/probe.js +64 -0
- package/src/render/pyramid.js +48 -0
- package/src/render/renderer.js +296 -0
- package/src/render/shaders/color.js +58 -0
- package/src/render/shaders/glass.js +241 -0
- package/src/render/shaders/ui.js +194 -0
- package/src/widgets/composite.js +355 -0
- package/src/widgets/controls.js +545 -0
- package/src/widgets/desktop.js +144 -0
- package/src/widgets/glass.js +326 -0
- package/src/widgets/primitives.js +355 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { rectContains } from './geometry.js'
|
|
2
|
+
|
|
3
|
+
const KEY_NAMES = {
|
|
4
|
+
' ': 'Space',
|
|
5
|
+
ArrowLeft: 'Left',
|
|
6
|
+
ArrowRight: 'Right',
|
|
7
|
+
ArrowUp: 'Up',
|
|
8
|
+
ArrowDown: 'Down',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function keyName(event) {
|
|
12
|
+
return KEY_NAMES[event.key] ?? event.key
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createInput(canvas) {
|
|
16
|
+
const queue = []
|
|
17
|
+
let hitList = []
|
|
18
|
+
let scrollTargets = []
|
|
19
|
+
let windowFocused = true
|
|
20
|
+
|
|
21
|
+
const frame = emptyFrame()
|
|
22
|
+
let pointer = { x: -1, y: -1, type: 'mouse' }
|
|
23
|
+
let active = null
|
|
24
|
+
let hot = null
|
|
25
|
+
let hotEntry = null
|
|
26
|
+
let pressOrigin = { x: 0, y: 0 }
|
|
27
|
+
|
|
28
|
+
function emptyFrame() {
|
|
29
|
+
return {
|
|
30
|
+
x: -1,
|
|
31
|
+
y: -1,
|
|
32
|
+
dx: 0,
|
|
33
|
+
dy: 0,
|
|
34
|
+
pointerType: 'mouse',
|
|
35
|
+
down: false,
|
|
36
|
+
pressed: new Set(),
|
|
37
|
+
released: new Set(),
|
|
38
|
+
clicked: new Set(),
|
|
39
|
+
pressedAnywhere: false,
|
|
40
|
+
releasedAnywhere: false,
|
|
41
|
+
hot: null,
|
|
42
|
+
active: null,
|
|
43
|
+
dragOrigin: { x: 0, y: 0 },
|
|
44
|
+
wheelX: 0,
|
|
45
|
+
wheelY: 0,
|
|
46
|
+
wheelTarget: null,
|
|
47
|
+
wheelTargetX: null,
|
|
48
|
+
pinch: 0,
|
|
49
|
+
keysDown: new Set(),
|
|
50
|
+
keysPressed: new Set(),
|
|
51
|
+
keysReleased: new Set(),
|
|
52
|
+
text: '',
|
|
53
|
+
windowFocused: true,
|
|
54
|
+
moved: false,
|
|
55
|
+
idle: true,
|
|
56
|
+
context: null,
|
|
57
|
+
contextEntry: null,
|
|
58
|
+
pressedEntry: null,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function position(event) {
|
|
63
|
+
const bounds = canvas.getBoundingClientRect()
|
|
64
|
+
return { x: event.clientX - bounds.left, y: event.clientY - bounds.top }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function topmost(x, y) {
|
|
68
|
+
for (let i = hitList.length - 1; i >= 0; i--) {
|
|
69
|
+
const entry = hitList[i]
|
|
70
|
+
if (!rectContains(entry.rect, x, y)) continue
|
|
71
|
+
if (entry.clip && !rectContains(entry.clip, x, y)) continue
|
|
72
|
+
return entry
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function innermostScroll(x, y, horizontal) {
|
|
78
|
+
let target = null
|
|
79
|
+
for (const entry of scrollTargets) {
|
|
80
|
+
if (entry.horizontal !== horizontal) continue
|
|
81
|
+
if (rectContains(entry.rect, x, y) && (!entry.clip || rectContains(entry.clip, x, y))) target = entry
|
|
82
|
+
}
|
|
83
|
+
return target
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function refreshHot() {
|
|
87
|
+
const entry = topmost(pointer.x, pointer.y)
|
|
88
|
+
hotEntry = entry
|
|
89
|
+
if (active) {
|
|
90
|
+
hot = entry && entry.id === active ? active : null
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
hot = entry ? entry.id : null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const handlers = {
|
|
97
|
+
pointerdown(event) {
|
|
98
|
+
if (event.button !== 0 && event.pointerType === 'mouse') return
|
|
99
|
+
pointer = { ...position(event), type: event.pointerType }
|
|
100
|
+
canvas.setPointerCapture?.(event.pointerId)
|
|
101
|
+
canvas.focus?.({ preventScroll: true })
|
|
102
|
+
queue.push({ type: 'down', x: pointer.x, y: pointer.y, pointerType: event.pointerType })
|
|
103
|
+
event.preventDefault()
|
|
104
|
+
},
|
|
105
|
+
pointermove(event) {
|
|
106
|
+
pointer = { ...position(event), type: event.pointerType }
|
|
107
|
+
queue.push({ type: 'move', x: pointer.x, y: pointer.y, pointerType: event.pointerType })
|
|
108
|
+
},
|
|
109
|
+
pointerup(event) {
|
|
110
|
+
if (event.button !== 0 && event.pointerType === 'mouse') return
|
|
111
|
+
pointer = { ...position(event), type: event.pointerType }
|
|
112
|
+
queue.push({ type: 'up', x: pointer.x, y: pointer.y })
|
|
113
|
+
},
|
|
114
|
+
contextmenu(event) {
|
|
115
|
+
pointer = { ...position(event), type: 'mouse' }
|
|
116
|
+
canvas.focus?.({ preventScroll: true })
|
|
117
|
+
queue.push({ type: 'context', x: pointer.x, y: pointer.y })
|
|
118
|
+
event.preventDefault()
|
|
119
|
+
},
|
|
120
|
+
pointercancel() {
|
|
121
|
+
queue.push({ type: 'up', x: pointer.x, y: pointer.y })
|
|
122
|
+
},
|
|
123
|
+
pointerleave() {
|
|
124
|
+
queue.push({ type: 'leave' })
|
|
125
|
+
},
|
|
126
|
+
wheel(event) {
|
|
127
|
+
const scale = event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? 400 : 1
|
|
128
|
+
const pos = position(event)
|
|
129
|
+
queue.push({ type: 'wheel', x: pos.x, y: pos.y, dx: event.deltaX * scale, dy: event.deltaY * scale, pinch: Boolean(event.ctrlKey) })
|
|
130
|
+
event.preventDefault()
|
|
131
|
+
},
|
|
132
|
+
keydown(event) {
|
|
133
|
+
const name = keyName(event)
|
|
134
|
+
queue.push({ type: 'keydown', key: name, repeat: event.repeat, meta: event.metaKey || event.ctrlKey, shift: event.shiftKey })
|
|
135
|
+
if (event.key.length === 1 && !event.metaKey && !event.ctrlKey) queue.push({ type: 'text', text: event.key })
|
|
136
|
+
if (name === 'Tab' || name === 'Space' || name.startsWith('Arrow')) event.preventDefault()
|
|
137
|
+
},
|
|
138
|
+
keyup(event) {
|
|
139
|
+
queue.push({ type: 'keyup', key: keyName(event) })
|
|
140
|
+
},
|
|
141
|
+
blur() {
|
|
142
|
+
windowFocused = false
|
|
143
|
+
queue.push({ type: 'up', x: pointer.x, y: pointer.y })
|
|
144
|
+
queue.push({ type: 'blur' })
|
|
145
|
+
},
|
|
146
|
+
focus() {
|
|
147
|
+
windowFocused = true
|
|
148
|
+
queue.push({ type: 'focus' })
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function attach() {
|
|
153
|
+
canvas.tabIndex = canvas.tabIndex >= 0 ? canvas.tabIndex : 0
|
|
154
|
+
canvas.style.touchAction = 'none'
|
|
155
|
+
canvas.style.outline = 'none'
|
|
156
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
157
|
+
const target = name === 'blur' || name === 'focus' ? window : canvas
|
|
158
|
+
target.addEventListener(name, handler, { passive: !['wheel', 'pointerdown', 'keydown', 'contextmenu'].includes(name) })
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function detach() {
|
|
163
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
164
|
+
const target = name === 'blur' || name === 'focus' ? window : canvas
|
|
165
|
+
target.removeEventListener(name, handler)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pending() {
|
|
170
|
+
return queue.length > 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function beginFrame() {
|
|
174
|
+
const next = emptyFrame()
|
|
175
|
+
next.idle = queue.length === 0
|
|
176
|
+
next.keysDown = frame.keysDown
|
|
177
|
+
const previousX = frame.x
|
|
178
|
+
const previousY = frame.y
|
|
179
|
+
let releaseActive = false
|
|
180
|
+
|
|
181
|
+
for (const event of queue) {
|
|
182
|
+
switch (event.type) {
|
|
183
|
+
case 'move':
|
|
184
|
+
pointer.x = event.x
|
|
185
|
+
pointer.y = event.y
|
|
186
|
+
next.pointerType = event.pointerType
|
|
187
|
+
refreshHot()
|
|
188
|
+
break
|
|
189
|
+
case 'down': {
|
|
190
|
+
pointer.x = event.x
|
|
191
|
+
pointer.y = event.y
|
|
192
|
+
next.pointerType = event.pointerType
|
|
193
|
+
active = null
|
|
194
|
+
refreshHot()
|
|
195
|
+
next.down = true
|
|
196
|
+
next.pressedAnywhere = true
|
|
197
|
+
next.pressedEntry = hotEntry
|
|
198
|
+
pressOrigin = { x: event.x, y: event.y }
|
|
199
|
+
if (hot) {
|
|
200
|
+
active = hot
|
|
201
|
+
next.pressed.add(hot)
|
|
202
|
+
}
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
case 'up': {
|
|
206
|
+
pointer.x = event.x
|
|
207
|
+
pointer.y = event.y
|
|
208
|
+
refreshHot()
|
|
209
|
+
next.releasedAnywhere = true
|
|
210
|
+
if (active) {
|
|
211
|
+
next.released.add(active)
|
|
212
|
+
if (hot === active) next.clicked.add(active)
|
|
213
|
+
releaseActive = true
|
|
214
|
+
}
|
|
215
|
+
next.down = false
|
|
216
|
+
break
|
|
217
|
+
}
|
|
218
|
+
case 'context':
|
|
219
|
+
pointer.x = event.x
|
|
220
|
+
pointer.y = event.y
|
|
221
|
+
refreshHot()
|
|
222
|
+
next.context = { x: event.x, y: event.y }
|
|
223
|
+
next.contextEntry = hotEntry
|
|
224
|
+
break
|
|
225
|
+
case 'leave':
|
|
226
|
+
if (!active) {
|
|
227
|
+
hot = null
|
|
228
|
+
hotEntry = null
|
|
229
|
+
}
|
|
230
|
+
break
|
|
231
|
+
case 'wheel': {
|
|
232
|
+
if (event.pinch) {
|
|
233
|
+
next.pinch += event.dy
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
next.wheelX += event.dx
|
|
237
|
+
next.wheelY += event.dy
|
|
238
|
+
const horizontal = Math.abs(event.dx) > Math.abs(event.dy)
|
|
239
|
+
const target = innermostScroll(event.x, event.y, horizontal)
|
|
240
|
+
if (horizontal) next.wheelTargetX = target ? target.id : null
|
|
241
|
+
else next.wheelTarget = target ? target.id : null
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
case 'keydown':
|
|
245
|
+
next.keysPressed.add(event.key)
|
|
246
|
+
next.keysDown.add(event.key)
|
|
247
|
+
if (event.meta) next.keysDown.add('Meta')
|
|
248
|
+
if (event.shift) next.keysDown.add('Shift')
|
|
249
|
+
else next.keysDown.delete('Shift')
|
|
250
|
+
break
|
|
251
|
+
case 'keyup':
|
|
252
|
+
next.keysReleased.add(event.key)
|
|
253
|
+
next.keysDown.delete(event.key)
|
|
254
|
+
if (event.key === 'Meta' || event.key === 'Control') next.keysDown.delete('Meta')
|
|
255
|
+
break
|
|
256
|
+
case 'text':
|
|
257
|
+
next.text += event.text
|
|
258
|
+
break
|
|
259
|
+
case 'blur':
|
|
260
|
+
next.keysDown.clear()
|
|
261
|
+
break
|
|
262
|
+
default:
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
queue.length = 0
|
|
267
|
+
|
|
268
|
+
if (!next.pressedAnywhere && !next.releasedAnywhere) refreshHot()
|
|
269
|
+
|
|
270
|
+
next.x = pointer.x
|
|
271
|
+
next.y = pointer.y
|
|
272
|
+
next.dx = previousX < 0 ? 0 : pointer.x - previousX
|
|
273
|
+
next.dy = previousY < 0 ? 0 : pointer.y - previousY
|
|
274
|
+
next.moved = next.dx !== 0 || next.dy !== 0
|
|
275
|
+
next.down = active !== null && !releaseActive ? true : next.down && !releaseActive
|
|
276
|
+
next.hot = hot
|
|
277
|
+
next.active = active
|
|
278
|
+
next.hotEntry = hotEntry
|
|
279
|
+
next.dragOrigin = pressOrigin
|
|
280
|
+
next.windowFocused = windowFocused
|
|
281
|
+
|
|
282
|
+
if (releaseActive) active = null
|
|
283
|
+
|
|
284
|
+
Object.assign(frame, next)
|
|
285
|
+
return frame
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function endFrame(list, scrolls) {
|
|
289
|
+
hitList = list
|
|
290
|
+
scrollTargets = scrolls
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { attach, detach, beginFrame, endFrame, pending, get frame() { return frame }, get hits() { return hitList }, get pointer() { return pointer } }
|
|
294
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { padding } from './geometry.js'
|
|
2
|
+
|
|
3
|
+
export function scaleNode(node, cx, cy, factor) {
|
|
4
|
+
if (factor === 1) return
|
|
5
|
+
node.rect = {
|
|
6
|
+
x: cx + (node.rect.x - cx) * factor,
|
|
7
|
+
y: cy + (node.rect.y - cy) * factor,
|
|
8
|
+
w: node.rect.w * factor,
|
|
9
|
+
h: node.rect.h * factor,
|
|
10
|
+
}
|
|
11
|
+
node.drawScale = (node.drawScale ?? 1) * factor
|
|
12
|
+
for (const child of node.children) scaleNode(child, cx, cy, factor)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function translateNode(node, dx, dy) {
|
|
16
|
+
if (dx === 0 && dy === 0) return
|
|
17
|
+
node.rect = { ...node.rect, x: node.rect.x + dx, y: node.rect.y + dy }
|
|
18
|
+
for (const child of node.children) translateNode(child, dx, dy)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function applyTransform(node) {
|
|
22
|
+
const transform = node.props.transform
|
|
23
|
+
if (!transform) return
|
|
24
|
+
for (const child of node.children) {
|
|
25
|
+
if (child.props.detached) continue
|
|
26
|
+
scaleNode(child, node.rect.x, node.rect.y, transform.scale ?? 1)
|
|
27
|
+
translateNode(child, transform.tx ?? 0, transform.ty ?? 0)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MAIN = { row: 'w', column: 'h' }
|
|
32
|
+
const CROSS = { row: 'h', column: 'w' }
|
|
33
|
+
|
|
34
|
+
function axisSize(node, axis) {
|
|
35
|
+
return axis === 'w' ? node.props.w : node.props.h
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mainAxisOf(node) {
|
|
39
|
+
const direction = node.parent?.props.direction ?? 'row'
|
|
40
|
+
return direction === 'column' ? 'h' : direction === 'row' ? 'w' : null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isFlexible(node, axis) {
|
|
44
|
+
const size = axisSize(node, axis)
|
|
45
|
+
return size === 'fill' || ((node.props.fr ?? 0) > 0 && mainAxisOf(node) === axis)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function fixed(node, axis) {
|
|
49
|
+
const size = axisSize(node, axis)
|
|
50
|
+
if (typeof size !== 'number') return null
|
|
51
|
+
if (node.props.reveal != null) return size * Math.max(0, Math.min(1, node.props.reveal))
|
|
52
|
+
return size
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function flowChildren(node) {
|
|
56
|
+
return node.children.filter((child) => child.props.x == null && child.props.y == null && !child.props.detached)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function measure(node, context) {
|
|
60
|
+
for (const child of node.children) measure(child, context)
|
|
61
|
+
const pad = padding(node.props.pad)
|
|
62
|
+
const gap = node.props.gap ?? 0
|
|
63
|
+
const direction = node.props.direction ?? 'row'
|
|
64
|
+
const flow = flowChildren(node)
|
|
65
|
+
|
|
66
|
+
let w = 0
|
|
67
|
+
let h = 0
|
|
68
|
+
if (node.intrinsic) {
|
|
69
|
+
const size = node.intrinsic(context)
|
|
70
|
+
w = size.w
|
|
71
|
+
h = size.h
|
|
72
|
+
} else if (direction === 'stack') {
|
|
73
|
+
for (const child of flow) {
|
|
74
|
+
w = Math.max(w, child.natural.w)
|
|
75
|
+
h = Math.max(h, child.natural.h)
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
const main = MAIN[direction]
|
|
79
|
+
const cross = CROSS[direction]
|
|
80
|
+
let mainTotal = 0
|
|
81
|
+
let crossMax = 0
|
|
82
|
+
let present = 0
|
|
83
|
+
for (const child of flow) {
|
|
84
|
+
mainTotal += child.natural[main]
|
|
85
|
+
crossMax = Math.max(crossMax, child.natural[cross])
|
|
86
|
+
if (child.natural[main] > 0) present += 1
|
|
87
|
+
}
|
|
88
|
+
mainTotal += Math.max(0, present - 1) * gap
|
|
89
|
+
if (main === 'w') {
|
|
90
|
+
w = mainTotal
|
|
91
|
+
h = crossMax
|
|
92
|
+
} else {
|
|
93
|
+
h = mainTotal
|
|
94
|
+
w = crossMax
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
w += pad[1] + pad[3]
|
|
98
|
+
h += pad[0] + pad[2]
|
|
99
|
+
|
|
100
|
+
const fixedW = fixed(node, 'w')
|
|
101
|
+
const fixedH = fixed(node, 'h')
|
|
102
|
+
node.natural = {
|
|
103
|
+
w: Math.max(node.props.minW ?? 0, fixedW ?? (isFlexible(node, 'w') ? node.props.minW ?? 0 : w)),
|
|
104
|
+
h: Math.max(node.props.minH ?? 0, fixedH ?? (isFlexible(node, 'h') ? node.props.minH ?? 0 : h)),
|
|
105
|
+
}
|
|
106
|
+
if (node.props.reveal != null) {
|
|
107
|
+
const reveal = Math.max(0, Math.min(1, node.props.reveal))
|
|
108
|
+
node.natural.w *= reveal
|
|
109
|
+
node.natural.h *= reveal
|
|
110
|
+
}
|
|
111
|
+
node.content = { w, h }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function alignOffset(align, available, size) {
|
|
115
|
+
if (align === 'center') return (available - size) / 2
|
|
116
|
+
if (align === 'end') return available - size
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function placeAbsolute(child, node, pad) {
|
|
121
|
+
const inner = innerRect(node.rect, pad)
|
|
122
|
+
const w = fixed(child, 'w') ?? (isFlexible(child, 'w') ? inner.w : child.natural.w)
|
|
123
|
+
const h = fixed(child, 'h') ?? (isFlexible(child, 'h') ? inner.h : child.natural.h)
|
|
124
|
+
const x = child.props.x ?? alignOffset(child.props.alignSelf ?? node.props.align ?? 'start', inner.w, w)
|
|
125
|
+
const y = child.props.y ?? alignOffset(child.props.alignSelf ?? node.props.align ?? 'start', inner.h, h)
|
|
126
|
+
child.rect = { x: inner.x + x, y: inner.y + y, w, h }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function innerRect(rect, pad) {
|
|
130
|
+
return {
|
|
131
|
+
x: rect.x + pad[3],
|
|
132
|
+
y: rect.y + pad[0],
|
|
133
|
+
w: Math.max(0, rect.w - pad[1] - pad[3]),
|
|
134
|
+
h: Math.max(0, rect.h - pad[0] - pad[2]),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function position(node, rect) {
|
|
139
|
+
node.rect = rect
|
|
140
|
+
const pad = padding(node.props.pad)
|
|
141
|
+
const inner = innerRect(rect, pad)
|
|
142
|
+
const direction = node.props.direction ?? 'row'
|
|
143
|
+
const flow = flowChildren(node)
|
|
144
|
+
const gap = node.props.gap ?? 0
|
|
145
|
+
const align = node.props.align ?? 'start'
|
|
146
|
+
|
|
147
|
+
for (const child of node.children) {
|
|
148
|
+
if (child.props.detached) continue
|
|
149
|
+
if (child.props.x != null || child.props.y != null) placeAbsolute(child, node, pad)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (direction === 'stack') {
|
|
153
|
+
for (const child of flow) {
|
|
154
|
+
const w = fixed(child, 'w') ?? (isFlexible(child, 'w') || align === 'stretch' ? inner.w : child.natural.w)
|
|
155
|
+
const h = fixed(child, 'h') ?? (isFlexible(child, 'h') || align === 'stretch' ? inner.h : child.natural.h)
|
|
156
|
+
const selfAlign = child.props.alignSelf ?? align
|
|
157
|
+
child.rect = {
|
|
158
|
+
x: inner.x + alignOffset(selfAlign, inner.w, w),
|
|
159
|
+
y: inner.y + alignOffset(selfAlign, inner.h, h),
|
|
160
|
+
w,
|
|
161
|
+
h,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const main = MAIN[direction]
|
|
166
|
+
const cross = CROSS[direction]
|
|
167
|
+
const mainAvailable = inner[main]
|
|
168
|
+
const crossAvailable = inner[cross]
|
|
169
|
+
let used = 0
|
|
170
|
+
let shares = 0
|
|
171
|
+
let present = 0
|
|
172
|
+
for (const child of flow) {
|
|
173
|
+
if (isFlexible(child, main)) {
|
|
174
|
+
shares += child.props.fr ?? 1
|
|
175
|
+
present += 1
|
|
176
|
+
} else {
|
|
177
|
+
const size = fixed(child, main) ?? child.natural[main]
|
|
178
|
+
used += size
|
|
179
|
+
if (size > 0) present += 1
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
used += Math.max(0, present - 1) * gap
|
|
183
|
+
const leftover = Math.max(0, mainAvailable - used)
|
|
184
|
+
const justify = node.props.justify ?? 'start'
|
|
185
|
+
let cursor = 0
|
|
186
|
+
let spacing = gap
|
|
187
|
+
if (shares === 0) {
|
|
188
|
+
if (justify === 'center') cursor = leftover / 2
|
|
189
|
+
else if (justify === 'end') cursor = leftover
|
|
190
|
+
else if (justify === 'between' && flow.length > 1) spacing = gap + leftover / (flow.length - 1)
|
|
191
|
+
}
|
|
192
|
+
for (const child of flow) {
|
|
193
|
+
const flexible = isFlexible(child, main)
|
|
194
|
+
const mainSize = flexible
|
|
195
|
+
? Math.max(child.natural[main], (leftover * (child.props.fr ?? 1)) / shares)
|
|
196
|
+
: (fixed(child, main) ?? child.natural[main])
|
|
197
|
+
const selfAlign = child.props.alignSelf ?? align
|
|
198
|
+
const crossSize =
|
|
199
|
+
fixed(child, cross) ??
|
|
200
|
+
(isFlexible(child, cross) || selfAlign === 'stretch' ? crossAvailable : Math.min(child.natural[cross], crossAvailable))
|
|
201
|
+
const crossOffset = alignOffset(selfAlign, crossAvailable, crossSize)
|
|
202
|
+
child.rect =
|
|
203
|
+
main === 'w'
|
|
204
|
+
? { x: inner.x + cursor, y: inner.y + crossOffset, w: mainSize, h: crossSize }
|
|
205
|
+
: { x: inner.x + crossOffset, y: inner.y + cursor, w: crossSize, h: mainSize }
|
|
206
|
+
cursor += mainSize + (mainSize > 0 || flexible ? spacing : 0)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const child of node.children) {
|
|
211
|
+
if (child.props.detached) continue
|
|
212
|
+
position(child, child.rect)
|
|
213
|
+
}
|
|
214
|
+
applyTransform(node)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function layout(root, viewport, context) {
|
|
218
|
+
measure(root, context)
|
|
219
|
+
position(root, viewport)
|
|
220
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const DEFAULT = { stiffness: 260, damping: 26, mass: 1, rest: 0.001 }
|
|
2
|
+
const MAX_STEP = 1 / 240
|
|
3
|
+
|
|
4
|
+
export const springs = {
|
|
5
|
+
snappy: { stiffness: 420, damping: 30, mass: 1 },
|
|
6
|
+
gentle: { stiffness: 170, damping: 22, mass: 1 },
|
|
7
|
+
bouncy: { stiffness: 300, damping: 16, mass: 1 },
|
|
8
|
+
lift: { stiffness: 320, damping: 24, mass: 1 },
|
|
9
|
+
morph: { stiffness: 240, damping: 26, mass: 1 },
|
|
10
|
+
presence: { stiffness: 200, damping: 24, mass: 1 },
|
|
11
|
+
reveal: { stiffness: 900, damping: 50, mass: 1 },
|
|
12
|
+
fade: { stiffness: 500, damping: 46, mass: 1 },
|
|
13
|
+
liquid: { stiffness: 140, damping: 24, mass: 1 },
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function springForDuration(milliseconds) {
|
|
17
|
+
const omega = 7.5 / Math.max(0.05, milliseconds / 1000)
|
|
18
|
+
return { stiffness: omega * omega, damping: 2 * omega, mass: 1 }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createSpringClock() {
|
|
22
|
+
let dt = 0
|
|
23
|
+
let animating = false
|
|
24
|
+
let reduceMotion = false
|
|
25
|
+
|
|
26
|
+
function tick(seconds) {
|
|
27
|
+
dt = Math.min(seconds, 1 / 20)
|
|
28
|
+
animating = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function step(spring, target, opts) {
|
|
32
|
+
const config = opts ?? DEFAULT
|
|
33
|
+
if (reduceMotion) {
|
|
34
|
+
spring.value = target
|
|
35
|
+
spring.velocity = 0
|
|
36
|
+
return target
|
|
37
|
+
}
|
|
38
|
+
const stiffness = config.stiffness ?? DEFAULT.stiffness
|
|
39
|
+
const damping = config.damping ?? DEFAULT.damping
|
|
40
|
+
const mass = config.mass ?? DEFAULT.mass
|
|
41
|
+
const rest = config.rest ?? DEFAULT.rest
|
|
42
|
+
const steps = Math.max(1, Math.ceil(dt / MAX_STEP))
|
|
43
|
+
const h = dt / steps
|
|
44
|
+
for (let i = 0; i < steps; i++) {
|
|
45
|
+
const displacement = spring.value - target
|
|
46
|
+
const force = -stiffness * displacement - damping * spring.velocity
|
|
47
|
+
spring.velocity += (force / mass) * h
|
|
48
|
+
spring.value += spring.velocity * h
|
|
49
|
+
}
|
|
50
|
+
const settled = Math.abs(spring.velocity) < rest && Math.abs(spring.value - target) < rest
|
|
51
|
+
if (settled) {
|
|
52
|
+
spring.value = target
|
|
53
|
+
spring.velocity = 0
|
|
54
|
+
} else {
|
|
55
|
+
animating = true
|
|
56
|
+
}
|
|
57
|
+
return spring.value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function animate(data, key, target, opts) {
|
|
61
|
+
let spring = data[key]
|
|
62
|
+
if (!spring) {
|
|
63
|
+
spring = { value: target, velocity: 0 }
|
|
64
|
+
data[key] = spring
|
|
65
|
+
return target
|
|
66
|
+
}
|
|
67
|
+
return step(spring, target, opts)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function animateFrom(data, key, initial, target, opts) {
|
|
71
|
+
if (!data[key]) data[key] = { value: initial, velocity: 0 }
|
|
72
|
+
return step(data[key], target, opts)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function ease(data, key, target, speed = 8) {
|
|
76
|
+
let current = data[key]
|
|
77
|
+
if (current == null) {
|
|
78
|
+
data[key] = target
|
|
79
|
+
return target
|
|
80
|
+
}
|
|
81
|
+
if (reduceMotion) {
|
|
82
|
+
data[key] = target
|
|
83
|
+
return target
|
|
84
|
+
}
|
|
85
|
+
const next = current + (target - current) * Math.min(1, speed * dt)
|
|
86
|
+
if (Math.abs(next - target) < 0.0005) {
|
|
87
|
+
data[key] = target
|
|
88
|
+
return target
|
|
89
|
+
}
|
|
90
|
+
animating = true
|
|
91
|
+
data[key] = next
|
|
92
|
+
return next
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
tick,
|
|
97
|
+
animate,
|
|
98
|
+
animateFrom,
|
|
99
|
+
ease,
|
|
100
|
+
get dt() {
|
|
101
|
+
return dt
|
|
102
|
+
},
|
|
103
|
+
get animating() {
|
|
104
|
+
return animating
|
|
105
|
+
},
|
|
106
|
+
get reduceMotion() {
|
|
107
|
+
return reduceMotion
|
|
108
|
+
},
|
|
109
|
+
set reduceMotion(value) {
|
|
110
|
+
reduceMotion = value
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function createStateStore() {
|
|
2
|
+
const entries = new Map()
|
|
3
|
+
let frame = 0
|
|
4
|
+
|
|
5
|
+
function get(id, init) {
|
|
6
|
+
let entry = entries.get(id)
|
|
7
|
+
if (!entry) {
|
|
8
|
+
entry = { seen: frame, data: init ? init() : {} }
|
|
9
|
+
entries.set(id, entry)
|
|
10
|
+
}
|
|
11
|
+
entry.seen = frame
|
|
12
|
+
return entry.data
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function peek(id) {
|
|
16
|
+
return entries.get(id)?.data ?? null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function has(id) {
|
|
20
|
+
return entries.has(id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function remove(id) {
|
|
24
|
+
entries.delete(id)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nextFrame() {
|
|
28
|
+
frame += 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function collect(keepFrames = 2) {
|
|
32
|
+
for (const [id, entry] of entries) {
|
|
33
|
+
if (frame - entry.seen > keepFrames && !entry.data.retained) entries.delete(id)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function unseen() {
|
|
38
|
+
const result = []
|
|
39
|
+
for (const [id, entry] of entries) {
|
|
40
|
+
if (entry.seen !== frame) result.push([id, entry.data])
|
|
41
|
+
}
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function size() {
|
|
46
|
+
return entries.size
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function keys() {
|
|
50
|
+
return [...entries.keys()]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { get, peek, has, remove, nextFrame, collect, unseen, size, keys }
|
|
54
|
+
}
|