@foxui/3d 0.4.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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/components/depth3d/Depth3D.svelte +36 -0
- package/dist/components/depth3d/Depth3D.svelte.d.ts +9 -0
- package/dist/components/depth3d/Scene.svelte +156 -0
- package/dist/components/depth3d/Scene.svelte.d.ts +16 -0
- package/dist/components/depth3d/index.d.ts +2 -0
- package/dist/components/depth3d/index.js +2 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/components/model-picker/base/ModelPicker.svelte +74 -0
- package/dist/components/model-picker/base/ModelPicker.svelte.d.ts +19 -0
- package/dist/components/model-picker/base/ModelPickerScene.svelte +62 -0
- package/dist/components/model-picker/base/ModelPickerScene.svelte.d.ts +8 -0
- package/dist/components/model-picker/index.d.ts +3 -0
- package/dist/components/model-picker/index.js +3 -0
- package/dist/components/model-picker/modal/ModalModelPicker.svelte +32 -0
- package/dist/components/model-picker/modal/ModalModelPicker.svelte.d.ts +17 -0
- package/dist/components/model-picker/popover/PopoverModelPicker.svelte +41 -0
- package/dist/components/model-picker/popover/PopoverModelPicker.svelte.d.ts +18 -0
- package/dist/components/voxel-art/VoxelArt.svelte +180 -0
- package/dist/components/voxel-art/VoxelArt.svelte.d.ts +20 -0
- package/dist/components/voxel-art/index.d.ts +1 -0
- package/dist/components/voxel-art/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +1 -0
- package/package.json +82 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License Copyright (c) 2025 flo-bit
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of
|
|
4
|
+
charge, to any person obtaining a copy of this software and associated
|
|
5
|
+
documentation files (the "Software"), to deal in the Software without
|
|
6
|
+
restriction, including without limitation the rights to use, copy, modify, merge,
|
|
7
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice
|
|
12
|
+
(including the next paragraph) shall be included in all copies or substantial
|
|
13
|
+
portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
16
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
17
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
18
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
19
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 🦊 fox ui
|
|
2
|
+
|
|
3
|
+
svelte 5 + tailwind 4 ui kit, 3d components
|
|
4
|
+
|
|
5
|
+
- [Depth 3D](https://flo-bit.dev/ui-kit/components/3d/depth-3d)
|
|
6
|
+
- [Model Picker](https://flo-bit.dev/ui-kit/components/3d/model-picker)
|
|
7
|
+
- [Voxel Art](https://flo-bit.dev/ui-kit/components/3d/voxel-art)
|
|
8
|
+
|
|
9
|
+
> **This is a public alpha release. Expect bugs and breaking changes.**
|
|
10
|
+
|
|
11
|
+
[See all components here](https://flo-bit.dev/ui-kit)
|
|
12
|
+
|
|
13
|
+
For a guide on how to use this ui kit, see the [Quickstart Guide](https://flo-bit.dev/ui-kit/docs/quick-start).
|
|
14
|
+
|
|
15
|
+
Read more about [the philosophy/aim of this project here](https://flo-bit.dev/ui-kit/docs/philosophy).
|
|
16
|
+
|
|
17
|
+
For more information about development, contributing and the like, see the main [README](https://github.com/flo-bit/ui-kit/blob/main/README.md).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Canvas } from '@threlte/core';
|
|
3
|
+
import Scene, { type Props } from './Scene.svelte';
|
|
4
|
+
import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui';
|
|
5
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
6
|
+
import { cn } from '@foxui/core';
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
class: className,
|
|
10
|
+
sceneProps,
|
|
11
|
+
...restProps
|
|
12
|
+
}: WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & {
|
|
13
|
+
sceneProps: Props;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div id="depth3d" class={cn('h-80 w-80', className)} {...restProps}>
|
|
18
|
+
<Canvas>
|
|
19
|
+
<Scene {...sceneProps} />
|
|
20
|
+
</Canvas>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- no js fallback -->
|
|
24
|
+
<noscript>
|
|
25
|
+
<style>
|
|
26
|
+
#depth3d {
|
|
27
|
+
display: none;
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
|
|
31
|
+
<img
|
|
32
|
+
src={sceneProps.image.image}
|
|
33
|
+
alt=""
|
|
34
|
+
class={cn('h-80 w-80 rounded-2xl object-cover', className)}
|
|
35
|
+
/>
|
|
36
|
+
</noscript>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Props } from './Scene.svelte';
|
|
2
|
+
import type { WithElementRef, WithoutChildrenOrChild } from 'bits-ui';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
type $$ComponentProps = WithElementRef<WithoutChildrenOrChild<HTMLAttributes<HTMLDivElement>>> & {
|
|
5
|
+
sceneProps: Props;
|
|
6
|
+
};
|
|
7
|
+
declare const Depth3D: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Depth3D = ReturnType<typeof Depth3D>;
|
|
9
|
+
export default Depth3D;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type Props = {
|
|
3
|
+
image: {
|
|
4
|
+
image: string;
|
|
5
|
+
depth: string;
|
|
6
|
+
};
|
|
7
|
+
rounded?: number;
|
|
8
|
+
cameraPosition?: [number, number, number];
|
|
9
|
+
rotationScale?: number;
|
|
10
|
+
rotationSpeed?: number;
|
|
11
|
+
detail?: number;
|
|
12
|
+
depthScale?: number;
|
|
13
|
+
mouseMovement?: boolean;
|
|
14
|
+
};
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import { T, useTask } from '@threlte/core';
|
|
19
|
+
import { useTexture, Align } from '@threlte/extras';
|
|
20
|
+
import { Spring } from 'svelte/motion';
|
|
21
|
+
|
|
22
|
+
import { Vector2, ShaderMaterial, PlaneGeometry, LinearSRGBColorSpace } from 'three';
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
image,
|
|
26
|
+
rounded = 0.2,
|
|
27
|
+
cameraPosition = [0, 0, 10],
|
|
28
|
+
rotationScale = 0.2,
|
|
29
|
+
rotationSpeed = 2,
|
|
30
|
+
detail = 50,
|
|
31
|
+
depthScale = 1.5,
|
|
32
|
+
mouseMovement = true
|
|
33
|
+
}: Props = $props();
|
|
34
|
+
|
|
35
|
+
const rotation = new Vector2(0.5, 0.5);
|
|
36
|
+
|
|
37
|
+
const geometry = new PlaneGeometry(7, 7, detail, detail);
|
|
38
|
+
|
|
39
|
+
let rotationX = new Spring(0);
|
|
40
|
+
let rotationY = new Spring(0);
|
|
41
|
+
|
|
42
|
+
let time = 0;
|
|
43
|
+
let mouseMoved = -1;
|
|
44
|
+
|
|
45
|
+
const map = useTexture(image.image, {
|
|
46
|
+
transform: (texture) => {
|
|
47
|
+
texture.colorSpace = LinearSRGBColorSpace;
|
|
48
|
+
return texture;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const depthMap = useTexture(image.depth, {
|
|
52
|
+
transform: (texture) => {
|
|
53
|
+
return texture;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const uniforms = {
|
|
58
|
+
uTexture: { type: 't', value: map },
|
|
59
|
+
depthMap: { type: 't', value: depthMap }
|
|
60
|
+
};
|
|
61
|
+
const material = new ShaderMaterial({
|
|
62
|
+
uniforms: uniforms,
|
|
63
|
+
vertexShader: `
|
|
64
|
+
varying vec2 vUv;
|
|
65
|
+
uniform sampler2D depthMap;
|
|
66
|
+
|
|
67
|
+
void main() {
|
|
68
|
+
vUv = uv;
|
|
69
|
+
// move z position based on the depth map
|
|
70
|
+
float depth = texture2D(depthMap, vUv).r;
|
|
71
|
+
vec3 newPosition = position + vec3(0.0, 0.0, depth * ${depthScale.toFixed(1)});
|
|
72
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
|
|
73
|
+
}`,
|
|
74
|
+
fragmentShader: `
|
|
75
|
+
uniform sampler2D uTexture;
|
|
76
|
+
varying vec2 vUv;
|
|
77
|
+
|
|
78
|
+
float sdRoundedRect(vec2 p, vec2 b, float r) {
|
|
79
|
+
vec2 q = abs(p) - b + vec2(r);
|
|
80
|
+
return length(max(q, 0.0)) - r;
|
|
81
|
+
}
|
|
82
|
+
${
|
|
83
|
+
rounded > 0.01
|
|
84
|
+
? `
|
|
85
|
+
void main() {
|
|
86
|
+
vec2 uv = (vUv * 2.0) - 1.0;
|
|
87
|
+
|
|
88
|
+
// Size of the rectangle (half-size)
|
|
89
|
+
vec2 rectSize = vec2(1, 1);
|
|
90
|
+
|
|
91
|
+
// Calculate distance to the edge of the rounded rectangle
|
|
92
|
+
float d = sdRoundedRect(uv, rectSize, ${rounded.toFixed(4)});
|
|
93
|
+
|
|
94
|
+
// Smooth transition for anti-aliasing
|
|
95
|
+
float aa = fwidth(d);
|
|
96
|
+
float alpha = smoothstep(0.0, aa, -d);
|
|
97
|
+
|
|
98
|
+
gl_FragColor = texture2D(uTexture, vUv) * alpha;
|
|
99
|
+
}
|
|
100
|
+
`
|
|
101
|
+
: `
|
|
102
|
+
|
|
103
|
+
void main() {
|
|
104
|
+
vec2 uv = (vUv * 2.0) - 1.0;
|
|
105
|
+
gl_FragColor = texture2D(uTexture, vUv);
|
|
106
|
+
}`
|
|
107
|
+
}
|
|
108
|
+
`
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
useTask((dt) => {
|
|
112
|
+
if (mouseMoved > 0) {
|
|
113
|
+
mouseMoved -= dt;
|
|
114
|
+
} else {
|
|
115
|
+
time += dt * rotationSpeed;
|
|
116
|
+
rotationX.set(Math.sin(time) * 0.5);
|
|
117
|
+
rotationY.set(Math.cos(time) * 0.5);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function onDocumentMouseMove(event: MouseEvent) {
|
|
122
|
+
if (!mouseMovement) return;
|
|
123
|
+
|
|
124
|
+
// convert to [-0.5, 0.5]
|
|
125
|
+
rotationX.set((event.clientX / window.innerWidth - 0.5) * 2);
|
|
126
|
+
rotationY.set((event.clientY / window.innerHeight - 0.5) * 2);
|
|
127
|
+
|
|
128
|
+
mouseMoved = 1;
|
|
129
|
+
}
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<svelte:window onmousemove={onDocumentMouseMove} />
|
|
133
|
+
|
|
134
|
+
<T.PerspectiveCamera makeDefault position={cameraPosition}></T.PerspectiveCamera>
|
|
135
|
+
|
|
136
|
+
{#await map then mapValue}
|
|
137
|
+
{#await depthMap then depthValue}
|
|
138
|
+
<Align>
|
|
139
|
+
<T.Mesh
|
|
140
|
+
rotation.x={rotationY.current * rotationScale}
|
|
141
|
+
rotation.y={rotationX.current * rotationScale}
|
|
142
|
+
scale.x={mapValue.image.width / mapValue.image.height}
|
|
143
|
+
>
|
|
144
|
+
<T is={geometry} />
|
|
145
|
+
<T
|
|
146
|
+
is={material}
|
|
147
|
+
uniforms={{
|
|
148
|
+
depthMap: { value: depthValue },
|
|
149
|
+
uTexture: { value: mapValue },
|
|
150
|
+
mouse: { value: rotation }
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
</T.Mesh>
|
|
154
|
+
</Align>
|
|
155
|
+
{/await}
|
|
156
|
+
{/await}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Props = {
|
|
2
|
+
image: {
|
|
3
|
+
image: string;
|
|
4
|
+
depth: string;
|
|
5
|
+
};
|
|
6
|
+
rounded?: number;
|
|
7
|
+
cameraPosition?: [number, number, number];
|
|
8
|
+
rotationScale?: number;
|
|
9
|
+
rotationSpeed?: number;
|
|
10
|
+
detail?: number;
|
|
11
|
+
depthScale?: number;
|
|
12
|
+
mouseMovement?: boolean;
|
|
13
|
+
};
|
|
14
|
+
declare const Scene: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type Scene = ReturnType<typeof Scene>;
|
|
16
|
+
export default Scene;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Canvas } from '@threlte/core';
|
|
3
|
+
import { View } from '@threlte/extras';
|
|
4
|
+
import Scene from './ModelPickerScene.svelte';
|
|
5
|
+
import { cn } from '@foxui/core';
|
|
6
|
+
import { CineonToneMapping } from 'three';
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
items,
|
|
10
|
+
alwaysRotate = false,
|
|
11
|
+
onselect,
|
|
12
|
+
canvasClasses = '',
|
|
13
|
+
class: className,
|
|
14
|
+
gridClasses,
|
|
15
|
+
maxColumns = 4
|
|
16
|
+
}: {
|
|
17
|
+
items: { path: string; label: string }[];
|
|
18
|
+
alwaysRotate?: boolean;
|
|
19
|
+
onselect?: ({ path, label }: { path: string; label: string }) => void;
|
|
20
|
+
canvasClasses?: string;
|
|
21
|
+
portalTo?: string;
|
|
22
|
+
class?: string;
|
|
23
|
+
gridClasses?: string;
|
|
24
|
+
maxColumns?: number;
|
|
25
|
+
} = $props();
|
|
26
|
+
|
|
27
|
+
let states = $state(
|
|
28
|
+
new Array<{ hover: boolean; dom: HTMLElement | undefined }>(items.length).fill({
|
|
29
|
+
hover: false,
|
|
30
|
+
dom: undefined
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<div class={cn('relative h-full w-full', className)}>
|
|
36
|
+
<div
|
|
37
|
+
class={cn(
|
|
38
|
+
'grid h-full w-full grid-cols-2 gap-4',
|
|
39
|
+
maxColumns > 2 ? 'md:grid-cols-3' : '',
|
|
40
|
+
maxColumns > 3 ? 'lg:grid-cols-4' : '',
|
|
41
|
+
gridClasses
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{#each items as item, index}
|
|
45
|
+
<button
|
|
46
|
+
id="item"
|
|
47
|
+
class="hover:bg-base-200/40 dark:hover:bg-base-800/40 m-4 inline-block cursor-pointer rounded-2xl p-4 transition-all duration-300 hover:scale-105"
|
|
48
|
+
onclick={() => {
|
|
49
|
+
states[index].hover = false;
|
|
50
|
+
onselect?.({ path: item.path, label: item.label });
|
|
51
|
+
}}
|
|
52
|
+
onpointerenter={() => {
|
|
53
|
+
states[index].hover = true;
|
|
54
|
+
}}
|
|
55
|
+
onpointerleave={() => {
|
|
56
|
+
states[index].hover = false;
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<div class="aspect-square" bind:this={states[index].dom}></div>
|
|
60
|
+
<div class="text-base-700 dark:text-base-300 text-sm">{item.label}</div>
|
|
61
|
+
</button>
|
|
62
|
+
{/each}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class={cn('pointer-events-none absolute inset-0 h-full w-full', canvasClasses)}>
|
|
66
|
+
<Canvas toneMapping={CineonToneMapping}>
|
|
67
|
+
{#each items as item, index}
|
|
68
|
+
<View dom={states[index].dom}>
|
|
69
|
+
<Scene path={item.path} hover={states[index].hover || alwaysRotate} />
|
|
70
|
+
</View>
|
|
71
|
+
{/each}
|
|
72
|
+
</Canvas>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
items: {
|
|
3
|
+
path: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}[];
|
|
6
|
+
alwaysRotate?: boolean;
|
|
7
|
+
onselect?: ({ path, label }: {
|
|
8
|
+
path: string;
|
|
9
|
+
label: string;
|
|
10
|
+
}) => void;
|
|
11
|
+
canvasClasses?: string;
|
|
12
|
+
portalTo?: string;
|
|
13
|
+
class?: string;
|
|
14
|
+
gridClasses?: string;
|
|
15
|
+
maxColumns?: number;
|
|
16
|
+
};
|
|
17
|
+
declare const ModelPicker: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
18
|
+
type ModelPicker = ReturnType<typeof ModelPicker>;
|
|
19
|
+
export default ModelPicker;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { T, useTask, useThrelte } from '@threlte/core';
|
|
3
|
+
import { GLTF } from '@threlte/extras';
|
|
4
|
+
import { onMount } from 'svelte';
|
|
5
|
+
import { Box3, Group, Object3D, Vector3 } from 'three';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
path,
|
|
9
|
+
hover = false
|
|
10
|
+
}: {
|
|
11
|
+
path: string;
|
|
12
|
+
scaleFactor?: number;
|
|
13
|
+
hover?: boolean;
|
|
14
|
+
} = $props();
|
|
15
|
+
|
|
16
|
+
let rotation = $state(0);
|
|
17
|
+
let group: Group | undefined = $state();
|
|
18
|
+
|
|
19
|
+
const { start, stop } = useTask((delta) => {
|
|
20
|
+
rotation += delta;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
$effect(() => {
|
|
24
|
+
if (hover) {
|
|
25
|
+
start();
|
|
26
|
+
} else {
|
|
27
|
+
stop();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const { renderer } = useThrelte();
|
|
32
|
+
|
|
33
|
+
onMount(() => {
|
|
34
|
+
renderer.toneMappingExposure = 0.7;
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<T.PerspectiveCamera makeDefault position={[0, 0, 2]} fov={50} near={0.1} far={10} />
|
|
39
|
+
|
|
40
|
+
<T.DirectionalLight args={[0xffffff, 2]} position={[-1, 1, 1]} />
|
|
41
|
+
<T.AmbientLight args={[0xffffff, 0.7]} />
|
|
42
|
+
|
|
43
|
+
<T.Group rotation={[0.5, rotation + 2.5, 0]}>
|
|
44
|
+
<T.Group bind:ref={group}>
|
|
45
|
+
<GLTF
|
|
46
|
+
url={path}
|
|
47
|
+
onload={(gltf: Group & { scene: Object3D }) => {
|
|
48
|
+
if (!group) return;
|
|
49
|
+
|
|
50
|
+
const box = new Box3().setFromObject(gltf.scene);
|
|
51
|
+
const size = box.getSize(new Vector3());
|
|
52
|
+
const center = box.getCenter(new Vector3());
|
|
53
|
+
|
|
54
|
+
let maxSize = Math.max(size.x, size.y, size.z);
|
|
55
|
+
let scale = 0.9 / maxSize;
|
|
56
|
+
|
|
57
|
+
group.scale.set(scale, scale, scale);
|
|
58
|
+
group.position.set(-center.x * scale, -center.y * scale, -center.z * scale);
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
</T.Group>
|
|
62
|
+
</T.Group>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
path: string;
|
|
3
|
+
scaleFactor?: number;
|
|
4
|
+
hover?: boolean;
|
|
5
|
+
};
|
|
6
|
+
declare const ModelPickerScene: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type ModelPickerScene = ReturnType<typeof ModelPickerScene>;
|
|
8
|
+
export default ModelPickerScene;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Modal } from '@foxui/core';
|
|
3
|
+
import { ModelPicker } from '..';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
items,
|
|
7
|
+
open = $bindable(),
|
|
8
|
+
alwaysRotate = false,
|
|
9
|
+
onselect,
|
|
10
|
+
canvasClasses = '',
|
|
11
|
+
title
|
|
12
|
+
}: {
|
|
13
|
+
open: boolean;
|
|
14
|
+
items: { path: string; label: string }[];
|
|
15
|
+
alwaysRotate?: boolean;
|
|
16
|
+
onselect?: ({ path, label }: { path: string; label: string }) => void;
|
|
17
|
+
canvasClasses?: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
} = $props();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<Modal bind:open class="mx-auto max-h-[80dvh] max-w-5xl overflow-hidden overflow-y-scroll" {title}>
|
|
23
|
+
<ModelPicker
|
|
24
|
+
{items}
|
|
25
|
+
{alwaysRotate}
|
|
26
|
+
onselect={({ path, label }) => {
|
|
27
|
+
onselect?.({ path, label });
|
|
28
|
+
open = false;
|
|
29
|
+
}}
|
|
30
|
+
{canvasClasses}
|
|
31
|
+
/>
|
|
32
|
+
</Modal>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
items: {
|
|
4
|
+
path: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}[];
|
|
7
|
+
alwaysRotate?: boolean;
|
|
8
|
+
onselect?: ({ path, label }: {
|
|
9
|
+
path: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}) => void;
|
|
12
|
+
canvasClasses?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
};
|
|
15
|
+
declare const ModalModelPicker: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
16
|
+
type ModalModelPicker = ReturnType<typeof ModalModelPicker>;
|
|
17
|
+
export default ModalModelPicker;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ModelPicker } from '../';
|
|
3
|
+
import { Popover } from '@foxui/core';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
items,
|
|
7
|
+
open = $bindable(false),
|
|
8
|
+
alwaysRotate = false,
|
|
9
|
+
onselect,
|
|
10
|
+
canvasClasses = '',
|
|
11
|
+
triggerClasses = '',
|
|
12
|
+
title
|
|
13
|
+
}: {
|
|
14
|
+
open?: boolean;
|
|
15
|
+
items: { path: string; label: string }[];
|
|
16
|
+
alwaysRotate?: boolean;
|
|
17
|
+
onselect?: ({ path, label }: { path: string; label: string }) => void;
|
|
18
|
+
canvasClasses?: string;
|
|
19
|
+
triggerClasses?: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
} = $props();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<Popover
|
|
25
|
+
bind:open
|
|
26
|
+
{triggerClasses}
|
|
27
|
+
side={'top'}
|
|
28
|
+
sideOffset={10}
|
|
29
|
+
triggerText={title}
|
|
30
|
+
class="mx-2 max-h-[60dvh] w-full max-w-[calc(100vw-1rem)] overflow-y-scroll"
|
|
31
|
+
>
|
|
32
|
+
<ModelPicker
|
|
33
|
+
{items}
|
|
34
|
+
{alwaysRotate}
|
|
35
|
+
onselect={({ path, label }) => {
|
|
36
|
+
onselect?.({ path, label });
|
|
37
|
+
open = false;
|
|
38
|
+
}}
|
|
39
|
+
{canvasClasses}
|
|
40
|
+
/>
|
|
41
|
+
</Popover>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
items: {
|
|
4
|
+
path: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}[];
|
|
7
|
+
alwaysRotate?: boolean;
|
|
8
|
+
onselect?: ({ path, label }: {
|
|
9
|
+
path: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}) => void;
|
|
12
|
+
canvasClasses?: string;
|
|
13
|
+
triggerClasses?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
};
|
|
16
|
+
declare const PopoverModelPicker: import("svelte").Component<$$ComponentProps, {}, "open">;
|
|
17
|
+
type PopoverModelPicker = ReturnType<typeof PopoverModelPicker>;
|
|
18
|
+
export default PopoverModelPicker;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// from https://tympanus.net/codrops/2025/03/03/css-meets-voxel-art-building-a-rendering-engine-with-stacked-grids/
|
|
3
|
+
// editor: https://voxels.layoutit.com/
|
|
4
|
+
|
|
5
|
+
interface VoxelArtData {
|
|
6
|
+
voxels: {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
z: number;
|
|
10
|
+
color: string;
|
|
11
|
+
}[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
rows = undefined,
|
|
16
|
+
columns = undefined,
|
|
17
|
+
stacks = undefined,
|
|
18
|
+
cubeSize = 20,
|
|
19
|
+
viewingAngle = 65,
|
|
20
|
+
colorMap = {
|
|
21
|
+
'050505': 'var(--color-accent-50)',
|
|
22
|
+
'101010': 'var(--color-accent-100)',
|
|
23
|
+
'202020': 'var(--color-accent-200)',
|
|
24
|
+
'303030': 'var(--color-accent-300)',
|
|
25
|
+
'404040': 'var(--color-accent-400)',
|
|
26
|
+
'505050': 'var(--color-accent-500)',
|
|
27
|
+
'606060': 'var(--color-accent-600)',
|
|
28
|
+
'707070': 'var(--color-accent-700)',
|
|
29
|
+
'808080': 'var(--color-accent-800)',
|
|
30
|
+
'909090': 'var(--color-accent-900)',
|
|
31
|
+
'959595': 'var(--color-accent-950)'
|
|
32
|
+
},
|
|
33
|
+
data
|
|
34
|
+
}: {
|
|
35
|
+
rows?: number;
|
|
36
|
+
columns?: number;
|
|
37
|
+
stacks?: number;
|
|
38
|
+
cubeSize?: number;
|
|
39
|
+
viewingAngle?: number;
|
|
40
|
+
colorMap?: Record<string, string>;
|
|
41
|
+
data: VoxelArtData;
|
|
42
|
+
} = $props();
|
|
43
|
+
|
|
44
|
+
let maxX = Math.max(...data.voxels.map((voxel) => voxel.x));
|
|
45
|
+
let maxY = Math.max(...data.voxels.map((voxel) => voxel.y));
|
|
46
|
+
let maxZ = Math.max(...data.voxels.map((voxel) => voxel.z));
|
|
47
|
+
|
|
48
|
+
if (rows === undefined) {
|
|
49
|
+
rows = maxX;
|
|
50
|
+
}
|
|
51
|
+
if (columns === undefined) {
|
|
52
|
+
columns = maxY;
|
|
53
|
+
}
|
|
54
|
+
if (stacks === undefined) {
|
|
55
|
+
stacks = maxZ + 1;
|
|
56
|
+
}
|
|
57
|
+
if (cubeSize === undefined) {
|
|
58
|
+
cubeSize = 20;
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<div
|
|
63
|
+
class="scene"
|
|
64
|
+
style="--columns: {columns}; --rows: {rows}; --stacks: {stacks}; --cube-size: {cubeSize}px; --viewing-angle: {viewingAngle}deg;"
|
|
65
|
+
>
|
|
66
|
+
<div class="floor">
|
|
67
|
+
{#each Array(stacks) as _, i}
|
|
68
|
+
<div class="z" style="transform: translateZ({i * cubeSize - stacks * cubeSize}px);">
|
|
69
|
+
{#each data.voxels as voxel}
|
|
70
|
+
{#if voxel.z === i}
|
|
71
|
+
{@render cube(
|
|
72
|
+
`grid-area: ${voxel.x} / ${voxel.y} / ${voxel.x + 1} / ${voxel.y + 1}; color: ${colorMap?.[voxel.color] ?? '#' + voxel.color};`
|
|
73
|
+
)}
|
|
74
|
+
{/if}
|
|
75
|
+
{/each}
|
|
76
|
+
</div>
|
|
77
|
+
{/each}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{#snippet cube(style: string)}
|
|
82
|
+
<div class="cube" {style}>
|
|
83
|
+
{#if viewingAngle < 90}
|
|
84
|
+
<div class="face top"></div>
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
<div class="face frontRight"></div>
|
|
88
|
+
<div class="face frontLeft"></div>
|
|
89
|
+
<div class="face backLeft"></div>
|
|
90
|
+
<div class="face backRight"></div>
|
|
91
|
+
|
|
92
|
+
{#if viewingAngle > 90}
|
|
93
|
+
<div class="face bottom"></div>
|
|
94
|
+
{/if}
|
|
95
|
+
</div>
|
|
96
|
+
{/snippet}
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
.scene {
|
|
100
|
+
perspective: 8000px;
|
|
101
|
+
min-height: calc((var(--stacks) + 5) * var(--cube-size));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.scene * {
|
|
105
|
+
transform-style: preserve-3d;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@keyframes spin {
|
|
109
|
+
0% {
|
|
110
|
+
transform: rotateX(var(--viewing-angle)) rotate(0deg);
|
|
111
|
+
}
|
|
112
|
+
100% {
|
|
113
|
+
transform: rotateX(var(--viewing-angle)) rotate(360deg);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.floor {
|
|
118
|
+
transform: rotateX(var(--viewing-angle)) rotate(45deg);
|
|
119
|
+
width: calc(var(--columns) * var(--cube-size));
|
|
120
|
+
height: calc(var(--rows) * var(--cube-size));
|
|
121
|
+
animation: spin 5s linear infinite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.cube {
|
|
125
|
+
position: relative;
|
|
126
|
+
transform: translateZ(calc(var(--cube-size) / 2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.face {
|
|
130
|
+
position: absolute;
|
|
131
|
+
background: currentColor;
|
|
132
|
+
inset: 0;
|
|
133
|
+
}
|
|
134
|
+
.face:after {
|
|
135
|
+
content: '';
|
|
136
|
+
display: block;
|
|
137
|
+
position: absolute;
|
|
138
|
+
inset: 0;
|
|
139
|
+
}
|
|
140
|
+
.face.frontRight:after {
|
|
141
|
+
background: rgba(0, 0, 0, 0.1);
|
|
142
|
+
}
|
|
143
|
+
.face.frontLeft:after {
|
|
144
|
+
background: rgba(0, 0, 0, 0.15);
|
|
145
|
+
}
|
|
146
|
+
.face.backLeft:after {
|
|
147
|
+
background: rgba(0, 0, 0, 0.2);
|
|
148
|
+
}
|
|
149
|
+
.face.backRight:after {
|
|
150
|
+
background: rgba(0, 0, 0, 0.25);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.face.top {
|
|
154
|
+
transform: translateZ(calc(var(--cube-size) / 2));
|
|
155
|
+
}
|
|
156
|
+
.face.frontRight {
|
|
157
|
+
transform: rotateY(90deg) translateZ(calc(var(--cube-size) / 2));
|
|
158
|
+
}
|
|
159
|
+
.face.frontLeft {
|
|
160
|
+
transform: rotateX(90deg) translateZ(calc(var(--cube-size) / -2));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.face.bottom {
|
|
164
|
+
transform: translateZ(calc(var(--cube-size) / -2));
|
|
165
|
+
}
|
|
166
|
+
.face.backLeft {
|
|
167
|
+
transform: rotateY(90deg) translateZ(calc(var(--cube-size) / -2));
|
|
168
|
+
}
|
|
169
|
+
.face.backRight {
|
|
170
|
+
transform: rotateX(90deg) translateZ(calc(var(--cube-size) / 2));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.z {
|
|
174
|
+
display: grid;
|
|
175
|
+
grid-template-columns: repeat(var(--columns), var(--cube-size));
|
|
176
|
+
grid-template-rows: repeat(var(--rows), var(--cube-size));
|
|
177
|
+
position: absolute;
|
|
178
|
+
inset: 0;
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface VoxelArtData {
|
|
2
|
+
voxels: {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
z: number;
|
|
6
|
+
color: string;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
rows?: number;
|
|
11
|
+
columns?: number;
|
|
12
|
+
stacks?: number;
|
|
13
|
+
cubeSize?: number;
|
|
14
|
+
viewingAngle?: number;
|
|
15
|
+
colorMap?: Record<string, string>;
|
|
16
|
+
data: VoxelArtData;
|
|
17
|
+
};
|
|
18
|
+
declare const VoxelArt: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
19
|
+
type VoxelArt = ReturnType<typeof VoxelArt>;
|
|
20
|
+
export default VoxelArt;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as VoxelArt } from './VoxelArt.svelte';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as VoxelArt } from './VoxelArt.svelte';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components/';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './components/';
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './index';
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@foxui/3d",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"sideEffects": [
|
|
10
|
+
"**/*.css"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/types.d.ts",
|
|
15
|
+
"svelte": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/types.d.ts",
|
|
19
|
+
"svelte": "./dist/index.js",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@eslint/compat": "^1.2.5",
|
|
22
|
+
"@eslint/js": "^9.18.0",
|
|
23
|
+
"@sveltejs/adapter-auto": "^6.0.0",
|
|
24
|
+
"@sveltejs/adapter-static": "^3.0.8",
|
|
25
|
+
"@sveltejs/kit": "^2.16.0",
|
|
26
|
+
"@sveltejs/package": "^2.3.11",
|
|
27
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
28
|
+
"@tailwindcss/forms": "^0.5.9",
|
|
29
|
+
"@tailwindcss/typography": "^0.5.15",
|
|
30
|
+
"@tailwindcss/vite": "^4.1.5",
|
|
31
|
+
"eslint": "^9.18.0",
|
|
32
|
+
"eslint-config-prettier": "^10.0.1",
|
|
33
|
+
"eslint-plugin-svelte": "^3.0.0",
|
|
34
|
+
"globals": "^16.0.0",
|
|
35
|
+
"prettier": "^3.4.2",
|
|
36
|
+
"prettier-plugin-svelte": "^3.3.3",
|
|
37
|
+
"prettier-plugin-tailwindcss": "^0.6.11",
|
|
38
|
+
"svelte": "^5.0.0",
|
|
39
|
+
"svelte-check": "^4.0.0",
|
|
40
|
+
"tailwindcss": "^4.1.5",
|
|
41
|
+
"typescript": "^5.0.0",
|
|
42
|
+
"typescript-eslint": "^8.20.0",
|
|
43
|
+
"vite": "^6.2.6"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@threlte/core": "^8.0.2",
|
|
47
|
+
"@threlte/extras": "^9.1.4",
|
|
48
|
+
"@types/three": "^0.176.0",
|
|
49
|
+
"bits-ui": "^1.4.3",
|
|
50
|
+
"three": "^0.176.0",
|
|
51
|
+
"@foxui/core": "0.4.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"svelte": ">=5",
|
|
55
|
+
"tailwindcss": ">=3"
|
|
56
|
+
},
|
|
57
|
+
"keywords": [
|
|
58
|
+
"svelte",
|
|
59
|
+
"ui-kit",
|
|
60
|
+
"3d",
|
|
61
|
+
"components"
|
|
62
|
+
],
|
|
63
|
+
"description": "ui kit - svelte 5 + tailwind 4 - 3d components",
|
|
64
|
+
"homepage": "https://flo-bit.dev/ui-kit",
|
|
65
|
+
"repository": {
|
|
66
|
+
"type": "git",
|
|
67
|
+
"url": "git+https://github.com/flo-bit/ui-kit.git"
|
|
68
|
+
},
|
|
69
|
+
"author": "flo-bit (http://flo-bit.dev/)",
|
|
70
|
+
"bugs": "https://github.com/flo-bit/ui-kit/issues",
|
|
71
|
+
"license": "MIT",
|
|
72
|
+
"scripts": {
|
|
73
|
+
"dev": "vite dev",
|
|
74
|
+
"build": "vite build && npm run prepack",
|
|
75
|
+
"build:package": "vite build && npm run prepack",
|
|
76
|
+
"preview": "vite preview",
|
|
77
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
78
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
79
|
+
"format": "prettier --write .",
|
|
80
|
+
"lint": "prettier --check . && eslint ."
|
|
81
|
+
}
|
|
82
|
+
}
|