Skip to content
This repository was archived by the owner on Feb 1, 2026. It is now read-only.

Commit ad916a2

Browse files
feat(Image): add component, playground, docs (#529)
* feat(Image): add component, playground, docs * refactor(Image): improve reactivity readability * docs(Image): add interactive demo * chore: fix lint --------- Co-authored-by: Alvaro Saburido <alvaro.saburido@gmail.com>
1 parent aa1e86a commit ad916a2

11 files changed

Lines changed: 460 additions & 0 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export default defineConfig({
7878
{ text: 'Edges', link: '/guide/abstractions/edges' },
7979
{ text: 'PositionalAudio', link: '/guide/abstractions/positional-audio' },
8080
{ text: 'AnimatedSprite', link: '/guide/abstractions/animated-sprite' },
81+
{ text: 'Image', link: '/guide/abstractions/image' },
8182
{ text: 'Billboard', link: '/guide/abstractions/billboard' },
8283
],
8384
},
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script setup lang="ts">
2+
import { NoToneMapping, SRGBColorSpace } from 'three'
3+
import { TresCanvas } from '@tresjs/core'
4+
import { Image, OrbitControls } from '@tresjs/cientos'
5+
6+
const gl = {
7+
clearColor: '#82DBC5',
8+
outputColorSpace: SRGBColorSpace,
9+
toneMapping: NoToneMapping,
10+
}
11+
12+
const URL_STUB = 'https://upload.wikimedia.org/wikipedia/commons/'
13+
14+
const URLS = [
15+
'f/f0/Cyanistes_caeruleus_Oulu_20150516.JPG',
16+
'3/36/Cyanistes_caeruleus_Oulu_20130323.JPG',
17+
'2/2e/Cyanistes_caeruleus_Oulu_20170507_02.jpg',
18+
].map(url => URL_STUB + url)
19+
</script>
20+
21+
<template>
22+
<TresCanvas
23+
v-bind="gl"
24+
>
25+
<TresPerspectiveCamera :position="[0, 0, 2]" />
26+
<OrbitControls />
27+
<Image :url="URLS[0]" :radius="0.2" :transparent="true" :position="[-1.5, 0, -1]" />
28+
<Image :url="URLS[1]" :radius="0.2" :transparent="true" />
29+
<Image :url="URLS[2]" :radius="0.2" :transparent="true" :position="[1.5, 0, -1]" />
30+
</TresCanvas>
31+
</template>

docs/component-list/components.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default [
1717
},
1818
{ text: 'Sampler', link: '/guide/abstractions/sampler' },
1919
{ text: 'PositionalAudio', link: '/guide/abstractions/positional-audio' },
20+
{ text: 'Image', link: '/guide/abstractions/image' },
2021
{ text: 'Billboard', link: '/guide/abstractions/billboard' },
2122
],
2223
},

docs/guide/abstractions/image.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Image
2+
3+
<DocsDemo>
4+
<ImageDemo />
5+
</DocsDemo>
6+
7+
`<Image />` is a shader-based component that optionally loads then displays an image texture on a default plane or on your custom geometry.
8+
9+
## Usage
10+
11+
<<< @/.vitepress/theme/components/ImageDemo.vue
12+
13+
## Props
14+
15+
::: info
16+
`<Image />` is a THREE.Mesh and most Mesh attributes can be used as props on the component.
17+
:::
18+
19+
| Prop | Description | Default |
20+
| :--------------- | :--------------------------------------------------- | ------------- |
21+
| `segments` | Number of divisions in the default geometry. | `1` |
22+
| `scale` | Scale of the geometry. `number \| [number, number]` | `1` |
23+
| `color` | Color multiplied into the image texture. | `'white'` |
24+
| `zoom` | Shrinks or enlarges the image texture. | `1` |
25+
| `radius` | Border radius applied to the image texture. (Intended for rectangular geometries. Use with `transparent`.) | `0` |
26+
| `grayscale` | Power of grayscale effect. 0 is off. 1 is full grayscale. | `0` |
27+
| `toneMapped` | Whether this material is tone mapped according to the renderers toneMapping settings. [See THREE.material.tonemapped](https://threejs.org/docs/?q=material#api/en/materials/Material.toneMapped) | `0` |
28+
| `transparent` | Whether the image material should be transparent. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent) | `false` |
29+
| `transparent` | Whether the image material should be transparent. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent) | `false` |
30+
| `opacity` | Opacity of the image material. [See THREE.material.transparent](https://threejs.org/docs/?q=material#api/en/materials/Material.transparent) | `1` |
31+
| `side` | THREE.Side of the image material. [See THREE.material.side](https://threejs.org/docs/?q=material#api/en/materials/Material.side) | `FrontSide` |
32+
| `texture` | Image texture to display on the geometry. | |
33+
| `url` | Image URL to load and display on the geometry. | |
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<script setup lang="ts">
2+
import { TresCanvas } from '@tresjs/core'
3+
import { Image, OrbitControls } from '@tresjs/cientos'
4+
import { Color, DoubleSide, FrontSide, NoToneMapping } from 'three'
5+
import { TresLeches, useControls } from '@tresjs/leches'
6+
import '@tresjs/leches/styles'
7+
8+
const URLS = [
9+
'https://upload.wikimedia.org/wikipedia/commons/1/13/20220713-great-tit.jpg',
10+
'https://upload.wikimedia.org/wikipedia/commons/0/00/Friendly_Robin.jpg',
11+
]
12+
13+
const c = useControls({
14+
url: 'https://upload.wikimedia.org/wikipedia/commons/d/d4/Mars_2009_Plouaret.jpg',
15+
segments: { value: 1, min: 1, max: 10, step: 1 },
16+
scaleX: { value: 1, min: 0, max: 3, step: 0.01 },
17+
scaleY: { value: 1, min: 0, max: 3, step: 0.01 },
18+
isRed: false,
19+
zoom: { value: 1, min: 0, max: 3, step: 0.01 },
20+
radius: { value: 0, min: 0, max: 1, step: 0.01 },
21+
grayscale: { value: 0, min: 0, max: 1, step: 0.01 },
22+
toneMapped: false,
23+
transparent: true,
24+
opacity: { value: 1, min: 0, max: 1, step: 0.01 },
25+
isDoubleSided: true,
26+
enabled: true,
27+
})
28+
29+
const opacity = shallowRef(1)
30+
const url = shallowRef(URLS[0])
31+
const sx = shallowRef(1)
32+
33+
let elapsed = 0
34+
setInterval(() => {
35+
elapsed += 0.01
36+
opacity.value = Math.abs(Math.sin(elapsed))
37+
url.value = Math.sin(elapsed) > 0 ? URLS[0] : URLS[1]
38+
sx.value = (Math.cos(elapsed) * 0.5 + 0.5)
39+
}, 1000 / 30)
40+
</script>
41+
42+
<template>
43+
<TresLeches />
44+
<TresCanvas :tone-mapping="NoToneMapping">
45+
<TresPerspectiveCamera />
46+
<OrbitControls />
47+
<Image
48+
:position-y="2"
49+
:url="url"
50+
:radius="opacity"
51+
:color="new Color('white')"
52+
>
53+
<TresBoxGeometry />
54+
</Image>
55+
<Image
56+
v-if="c.enabled.value.value"
57+
:url="c.url.value.value"
58+
:segments="c.segments.value.value"
59+
:scale="[c.scaleX.value.value, c.scaleY.value.value]"
60+
:color="c.isRed.value.value ? '#F00' : '#FFF'"
61+
:zoom="c.zoom.value.value"
62+
:radius="c.radius.value.value"
63+
:grayscale="c.grayscale.value.value"
64+
:tone-mapped="c.toneMapped.value.value"
65+
:transparent="c.transparent.value.value"
66+
:opacity="c.opacity.value.value"
67+
:side="c.isDoubleSided.value.value ? DoubleSide : FrontSide"
68+
/>
69+
<Image
70+
:position-x="2"
71+
:scale="[sx, 1]"
72+
:url="url"
73+
/>
74+
<Image
75+
:position-y="-2"
76+
:scale="sx"
77+
:url="url"
78+
>
79+
<TresCircleGeometry />
80+
</Image>
81+
82+
<Image
83+
:position-x="-2"
84+
:url="url"
85+
:radius="opacity"
86+
:transparent="true"
87+
/>
88+
</TresCanvas>
89+
</template>

playground/vue/src/router/routes/abstractions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ export const abstractionsRoutes = [
6464
name: 'AnimatedSprite',
6565
component: () => import('../../pages/abstractions/AnimatedSpriteDemo.vue'),
6666
},
67+
{
68+
path: '/abstractions/image',
69+
name: 'Image',
70+
component: () => import('../../pages/abstractions/ImageDemo.vue'),
71+
},
6772
{
6873
path: '/abstractions/billboard',
6974
name: 'Billboard',
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
import { shallowRef } from 'vue'
3+
import { extend } from '@tresjs/core'
4+
import ImageMaterial from './ImageMaterialImpl'
5+
6+
extend({ ImageMaterial })
7+
8+
const materialRef = shallowRef()
9+
10+
defineExpose({ instance: materialRef })
11+
</script>
12+
13+
<template>
14+
<TresImageMaterial ref="materialRef" />
15+
</template>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { shaderMaterial } from './../../../utils/shaderMaterial'
2+
import { Color, Vector2 } from 'three'
3+
4+
/**
5+
* NOTE: Source:
6+
* https://threejs.org/docs/?q=material#api/en/materials/Material.transparent
7+
*/
8+
const imageMaterialImpl = shaderMaterial(
9+
{
10+
color: /* @__PURE__ */ new Color('white'),
11+
scale: /* @__PURE__ */ new Vector2(1, 1),
12+
imageBounds: /* @__PURE__ */ new Vector2(1, 1),
13+
resolution: 1024,
14+
map: null,
15+
zoom: 1,
16+
radius: 0,
17+
grayscale: 0,
18+
opacity: 1,
19+
},
20+
/* glsl */ `
21+
varying vec2 vUv;
22+
varying vec2 vPos;
23+
void main() {
24+
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.);
25+
vUv = uv;
26+
vPos = position.xy;
27+
}
28+
`,
29+
/* glsl */ `
30+
// mostly from https://gist.github.com/statico/df64c5d167362ecf7b34fca0b1459a44
31+
varying vec2 vUv;
32+
varying vec2 vPos;
33+
uniform vec2 scale;
34+
uniform vec2 imageBounds;
35+
uniform float resolution;
36+
uniform vec3 color;
37+
uniform sampler2D map;
38+
uniform float radius;
39+
uniform float zoom;
40+
uniform float grayscale;
41+
uniform float opacity;
42+
const vec3 luma = vec3(.299, 0.587, 0.114);
43+
vec4 toGrayscale(vec4 color, float intensity) {
44+
return vec4(mix(color.rgb, vec3(dot(color.rgb, luma)), intensity), color.a);
45+
}
46+
vec2 aspect(vec2 size) {
47+
return size / min(size.x, size.y);
48+
}
49+
50+
const float PI = 3.14159265;
51+
52+
// from https://iquilezles.org/articles/distfunctions
53+
float udRoundBox( vec2 p, vec2 b, float r ) {
54+
return length(max(abs(p)-b+r,0.0))-r;
55+
}
56+
57+
void main() {
58+
vec2 s = aspect(scale);
59+
vec2 i = aspect(imageBounds);
60+
float rs = s.x / s.y;
61+
float ri = i.x / i.y;
62+
vec2 new = rs < ri ? vec2(i.x * s.y / i.y, s.y) : vec2(s.x, i.y * s.x / i.x);
63+
vec2 offset = (rs < ri ? vec2((new.x - s.x) / 2.0, 0.0) : vec2(0.0, (new.y - s.y) / 2.0)) / new;
64+
vec2 uv = vUv * s / new + offset;
65+
vec2 zUv = (uv - vec2(0.5, 0.5)) / zoom + vec2(0.5, 0.5);
66+
67+
vec2 res = vec2(scale * resolution);
68+
vec2 halfRes = 0.5 * res;
69+
float b = udRoundBox(vUv.xy * res - halfRes, halfRes, resolution * radius);
70+
vec3 a = mix(vec3(1.0,0.0,0.0), vec3(0.0,0.0,0.0), smoothstep(0.0, 1.0, b));
71+
gl_FragColor = toGrayscale(texture2D(map, zUv) * vec4(color, opacity * a), grayscale);
72+
73+
#include <tonemapping_fragment>
74+
#include <colorspace_fragment>
75+
}
76+
`,
77+
)
78+
79+
export default imageMaterialImpl

0 commit comments

Comments
 (0)