@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.
Files changed (28) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/components/depth3d/Depth3D.svelte +36 -0
  4. package/dist/components/depth3d/Depth3D.svelte.d.ts +9 -0
  5. package/dist/components/depth3d/Scene.svelte +156 -0
  6. package/dist/components/depth3d/Scene.svelte.d.ts +16 -0
  7. package/dist/components/depth3d/index.d.ts +2 -0
  8. package/dist/components/depth3d/index.js +2 -0
  9. package/dist/components/index.d.ts +3 -0
  10. package/dist/components/index.js +3 -0
  11. package/dist/components/model-picker/base/ModelPicker.svelte +74 -0
  12. package/dist/components/model-picker/base/ModelPicker.svelte.d.ts +19 -0
  13. package/dist/components/model-picker/base/ModelPickerScene.svelte +62 -0
  14. package/dist/components/model-picker/base/ModelPickerScene.svelte.d.ts +8 -0
  15. package/dist/components/model-picker/index.d.ts +3 -0
  16. package/dist/components/model-picker/index.js +3 -0
  17. package/dist/components/model-picker/modal/ModalModelPicker.svelte +32 -0
  18. package/dist/components/model-picker/modal/ModalModelPicker.svelte.d.ts +17 -0
  19. package/dist/components/model-picker/popover/PopoverModelPicker.svelte +41 -0
  20. package/dist/components/model-picker/popover/PopoverModelPicker.svelte.d.ts +18 -0
  21. package/dist/components/voxel-art/VoxelArt.svelte +180 -0
  22. package/dist/components/voxel-art/VoxelArt.svelte.d.ts +20 -0
  23. package/dist/components/voxel-art/index.d.ts +1 -0
  24. package/dist/components/voxel-art/index.js +1 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/types.d.ts +1 -0
  28. 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,2 @@
1
+ export { default as Depth3D } from './Depth3D.svelte';
2
+ export { default } from './Depth3D.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as Depth3D } from './Depth3D.svelte';
2
+ export { default } from './Depth3D.svelte';
@@ -0,0 +1,3 @@
1
+ export * from './depth3d';
2
+ export * from './model-picker';
3
+ export * from './voxel-art';
@@ -0,0 +1,3 @@
1
+ export * from './depth3d';
2
+ export * from './model-picker';
3
+ export * from './voxel-art';
@@ -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,3 @@
1
+ export { default as ModelPicker } from './base/ModelPicker.svelte';
2
+ export { default as ModalModelPicker } from './modal/ModalModelPicker.svelte';
3
+ export { default as PopoverModelPicker } from './popover/PopoverModelPicker.svelte';
@@ -0,0 +1,3 @@
1
+ export { default as ModelPicker } from './base/ModelPicker.svelte';
2
+ export { default as ModalModelPicker } from './modal/ModalModelPicker.svelte';
3
+ export { default as PopoverModelPicker } from './popover/PopoverModelPicker.svelte';
@@ -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';
@@ -0,0 +1 @@
1
+ export * from './components/';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from './components/';
@@ -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
+ }