├── postcss.config.cjs
├── src
├── scripts
│ ├── Gui.ts
│ ├── shader
│ │ ├── quad.vs
│ │ └── effect.fs
│ ├── entry.ts
│ ├── core
│ │ ├── ExtendedMaterials.ts
│ │ ├── BackBuffer.ts
│ │ ├── FrameBuffer.ts
│ │ └── Three.ts
│ ├── Mouse2D.ts
│ └── Canvas.ts
├── vite-env.d.ts
├── styles
│ ├── imports.scss
│ ├── mixins
│ │ ├── fonts.scss
│ │ └── media.scss
│ ├── entry.scss
│ ├── lenis.scss
│ ├── global.scss
│ └── components
│ │ └── link.scss
├── index.html
└── typescript.svg
├── public
├── textures
│ └── azulejos.webp
└── vite.svg
├── .prettierrc.cjs
├── README.md
├── .gitignore
├── vite.config.ts
├── tsconfig.json
├── package.json
└── .github
└── workflows
└── deploy.yml
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [require('autoprefixer')],
3 | }
4 |
--------------------------------------------------------------------------------
/src/scripts/Gui.ts:
--------------------------------------------------------------------------------
1 | import GUI from 'lil-gui'
2 |
3 | export const gui = new GUI()
4 | gui.close()
5 |
--------------------------------------------------------------------------------
/public/textures/azulejos.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nemutas/azulejo/HEAD/public/textures/azulejos.webp
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/src/styles/imports.scss:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300&display=swap');
2 |
--------------------------------------------------------------------------------
/src/styles/mixins/fonts.scss:
--------------------------------------------------------------------------------
1 | @mixin poppins($weight: 300) {
2 | font-family: 'Poppins', sans-serif;
3 | font-weight: $weight;
4 | }
5 |
--------------------------------------------------------------------------------
/src/styles/entry.scss:
--------------------------------------------------------------------------------
1 | @use 'ress';
2 | @use './lenis.scss';
3 | @use './imports.scss';
4 | @use './global.scss';
5 | // components
6 | @use './components/link.scss';
7 |
--------------------------------------------------------------------------------
/src/scripts/shader/quad.vs:
--------------------------------------------------------------------------------
1 | #version 300 es
2 |
3 | in vec3 position;
4 | in vec2 uv;
5 |
6 | out vec2 vUv;
7 |
8 | void main() {
9 | vUv = uv;
10 | gl_Position = vec4(position, 1.0);
11 | }
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("prettier").Config} */
2 | module.exports = {
3 | tabWidth: 2,
4 | useTabs: false,
5 | semi: false,
6 | trailingComma: 'all',
7 | singleQuote: true,
8 | printWidth: 160,
9 | }
10 |
--------------------------------------------------------------------------------
/src/scripts/entry.ts:
--------------------------------------------------------------------------------
1 | import { Canvas } from './Canvas'
2 |
3 | const canvas = new Canvas(document.querySelector('.webgl-canvas')!)
4 |
5 | window.addEventListener('beforeunload', () => {
6 | canvas.dispose()
7 | })
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # About
2 |
3 | inspired by https://robert.leitl.dev/artifacts/azulejos/#/object/cylinder .
4 |
5 | https://nemutas.github.io/azulejo/
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/styles/mixins/media.scss:
--------------------------------------------------------------------------------
1 | @mixin pc {
2 | @media screen and (min-width: 751px) {
3 | @content;
4 | }
5 | }
6 |
7 | @mixin sp {
8 | @media screen and (max-width: 750px) {
9 | @content;
10 | }
11 | }
12 |
13 | @mixin hoverable {
14 | @media (hover: hover) and (pointer: fine) {
15 | @content;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/src/styles/lenis.scss:
--------------------------------------------------------------------------------
1 | html.lenis {
2 | height: auto;
3 | }
4 |
5 | .lenis.lenis-smooth {
6 | scroll-behavior: auto !important;
7 | }
8 |
9 | .lenis.lenis-smooth [data-lenis-prevent] {
10 | overscroll-behavior: contain;
11 | }
12 |
13 | .lenis.lenis-stopped {
14 | overflow: hidden;
15 | }
16 |
17 | .lenis.lenis-scrolling iframe {
18 | pointer-events: none;
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import glsl from 'vite-plugin-glsl'
3 |
4 | export default defineConfig(() => {
5 | return {
6 | root: './src',
7 | publicDir: '../public',
8 | base: '/azulejo/',
9 | build: {
10 | outDir: '../dist',
11 | },
12 | plugins: [glsl()],
13 | server: {
14 | host: true,
15 | },
16 | }
17 | })
18 |
--------------------------------------------------------------------------------
/src/scripts/core/ExtendedMaterials.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 |
3 | export class RawShaderMaterial extends THREE.RawShaderMaterial {
4 | constructor(parameters: THREE.ShaderMaterialParameters) {
5 | super(parameters)
6 | this.preprocess()
7 | }
8 |
9 | private preprocess() {
10 | if (this.glslVersion === '300 es') {
11 | this.vertexShader = this.vertexShader.replace('#version 300 es', '')
12 | this.fragmentShader = this.fragmentShader.replace('#version 300 es', '')
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "module": "ESNext",
6 | "lib": ["ES2023", "DOM", "DOM.Iterable"],
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true
21 | },
22 | "include": ["src"]
23 | }
24 |
--------------------------------------------------------------------------------
/src/styles/global.scss:
--------------------------------------------------------------------------------
1 | @use './mixins/media.scss' as *;
2 | @use './mixins/fonts.scss' as fonts;
3 |
4 | html {
5 | font-size: calc(1000vw / 1500);
6 | @include sp {
7 | font-size: calc(1000vw / 750);
8 | }
9 | height: 100%;
10 | background: #ebeae8;
11 | @include fonts.poppins;
12 | }
13 |
14 | body,
15 | main {
16 | position: relative;
17 | width: 100%;
18 | height: 100%;
19 | }
20 |
21 | a {
22 | color: inherit;
23 | text-decoration: none;
24 | }
25 |
26 | ul {
27 | list-style: none;
28 | }
29 |
30 | .webgl-canvas {
31 | position: fixed;
32 | top: 0;
33 | width: 100%;
34 | height: 100lvh;
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "azulejo",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "start": "vite",
9 | "build": "tsc && vite build",
10 | "preview": "vite preview"
11 | },
12 | "devDependencies": {
13 | "@types/three": "^0.160.0",
14 | "autoprefixer": "^10.4.15",
15 | "prettier": "^3.0.3",
16 | "ress": "^5.0.2",
17 | "sass": "^1.67.0",
18 | "typescript": "^5.0.2",
19 | "vite": "^4.4.5",
20 | "vite-plugin-glsl": "^1.1.2"
21 | },
22 | "dependencies": {
23 | "lil-gui": "^0.18.2",
24 | "three": "^0.160.0"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/scripts/core/BackBuffer.ts:
--------------------------------------------------------------------------------
1 | import { FrameBuffer, Options } from './FrameBuffer'
2 |
3 | export abstract class BackBuffer extends FrameBuffer {
4 | private readonly rt2: THREE.WebGLRenderTarget
5 | private prev: THREE.WebGLRenderTarget
6 | private current: THREE.WebGLRenderTarget
7 |
8 | constructor(renderer: THREE.WebGLRenderer, material: THREE.RawShaderMaterial, options?: Options) {
9 | super(renderer, material, options)
10 |
11 | this.rt2 = this.createRenderTarget()
12 | this.prev = this.renderTarget
13 | this.current = this.rt2
14 | }
15 |
16 | resize() {
17 | super.resize()
18 | this.rt2.setSize(this.size.width, this.size.height)
19 | }
20 |
21 | get backBuffer() {
22 | return this.prev.texture
23 | }
24 |
25 | private swap() {
26 | this.current = this.current === this.renderTarget ? this.rt2 : this.renderTarget
27 | this.prev = this.current === this.renderTarget ? this.rt2 : this.renderTarget
28 | }
29 |
30 | render(..._args: any) {
31 | this.renderer.setRenderTarget(this.current)
32 | this.renderer.render(this.scene, this.camera)
33 |
34 | this.swap()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/styles/components/link.scss:
--------------------------------------------------------------------------------
1 | @use '../mixins/media.scss' as *;
2 |
3 | .links {
4 | --color: #000;
5 |
6 | position: absolute;
7 | bottom: 1rem;
8 | right: 1rem;
9 | font-size: max(1.2rem, 15px);
10 | line-height: 1;
11 | color: var(--color);
12 | user-select: none;
13 | @include sp {
14 | bottom: 2rem;
15 | right: 2rem;
16 | font-size: min(3rem, 15px);
17 | }
18 |
19 | & > * {
20 | display: block;
21 | }
22 |
23 | & > *:last-child {
24 | margin-top: 0.5rem;
25 | @include sp {
26 | margin-top: 1rem;
27 | }
28 | }
29 |
30 | a {
31 | position: relative;
32 | width: fit-content;
33 | margin-left: auto;
34 |
35 | @include hoverable {
36 | &::after {
37 | content: '';
38 | position: absolute;
39 | bottom: 0;
40 | left: 0;
41 | width: 100%;
42 | height: 1px;
43 | background: var(--color);
44 | transform: scale(0, 1);
45 | transform-origin: right top;
46 | transition: transform 0.3s;
47 | }
48 | &:hover {
49 | &::after {
50 | transform-origin: left top;
51 | transform: scale(1, 1);
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/typescript.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # 静的コンテンツを GitHub Pages にデプロイするためのシンプルなワークフロー
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # デフォルトブランチを対象としたプッシュ時にで実行されます
6 | push:
7 | branches: ['main']
8 | paths-ignore: ['**/README.md']
9 |
10 | # Actions タブから手動でワークフローを実行できるようにします
11 | workflow_dispatch:
12 |
13 | # GITHUB_TOKEN のパーミッションを設定し、GitHub Pages へのデプロイを許可します
14 | permissions:
15 | contents: read
16 | pages: write
17 | id-token: write
18 |
19 | # 1 つの同時デプロイメントを可能にする
20 | concurrency:
21 | group: 'pages'
22 | cancel-in-progress: true
23 |
24 | jobs:
25 | # デプロイするだけなので、単一のデプロイジョブ
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v3
34 | - name: Set up Node
35 | uses: actions/setup-node@v3
36 | with:
37 | node-version: 18
38 | cache: 'npm'
39 | - name: Install dependencies
40 | run: npm install
41 | - name: Build
42 | run: npm run build
43 | - name: Setup Pages
44 | uses: actions/configure-pages@v3
45 | - name: Upload artifact
46 | uses: actions/upload-pages-artifact@v1
47 | with:
48 | # dist リポジトリのアップロード
49 | path: './dist'
50 | - name: Deploy to GitHub Pages
51 | id: deployment
52 | uses: actions/deploy-pages@v1
53 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/scripts/Mouse2D.ts:
--------------------------------------------------------------------------------
1 | class Mouse2D {
2 | readonly position: [number, number] = [99999, 99999]
3 | readonly prevPosition: [number, number] = [99999, 99999]
4 |
5 | constructor() {
6 | window.addEventListener('mousemove', this.handleMouseMove)
7 | window.addEventListener('touchmove', this.handleTouchMove)
8 | }
9 |
10 | private handleMouseMove = (e: MouseEvent) => {
11 | this.prevPosition[0] = this.position[0]
12 | this.prevPosition[1] = this.position[1]
13 | this.position[0] = (e.clientX / window.innerWidth) * 2 - 1
14 | this.position[1] = -1 * ((e.clientY / window.innerHeight) * 2 - 1)
15 | }
16 |
17 | private handleTouchMove = (e: TouchEvent) => {
18 | const { pageX, pageY } = e.touches[0]
19 | this.prevPosition[0] = this.position[0]
20 | this.prevPosition[1] = this.position[1]
21 | this.position[0] = (pageX / window.innerWidth) * 2 - 1
22 | this.position[1] = -1 * ((pageY / window.innerHeight) * 2 - 1)
23 | }
24 |
25 | lerp(t: number) {
26 | this.prevPosition[0] = this.prevPosition[0] * (1 - t) + this.position[0] * t
27 | this.prevPosition[1] = this.prevPosition[1] * (1 - t) + this.position[1] * t
28 | return [this.position[0] - this.prevPosition[0], this.position[1] - this.prevPosition[1]]
29 | }
30 |
31 | dispose() {
32 | window.removeEventListener('mousemove', this.handleMouseMove)
33 | window.removeEventListener('touchmove', this.handleTouchMove)
34 | }
35 | }
36 |
37 | export const mouse2d = new Mouse2D()
38 |
--------------------------------------------------------------------------------
/src/scripts/shader/effect.fs:
--------------------------------------------------------------------------------
1 | #version 300 es
2 | precision highp float;
3 |
4 | in vec2 vUv;
5 | out vec4 outColor;
6 |
7 | uniform sampler2D uSource;
8 | uniform vec2 uResolution;
9 | uniform sampler2D uMap;
10 | uniform vec2 uMapPx;
11 |
12 | #define sat(v) clamp(v, 0.0, 1.0)
13 |
14 | vec3 hash(vec3 v) {
15 | uvec3 x = floatBitsToUint(v + vec3(0.1, 0.2, 0.3));
16 | x = (x >> 8 ^ x.yzx) * 0x456789ABu;
17 | x = (x >> 8 ^ x.yzx) * 0x6789AB45u;
18 | x = (x >> 8 ^ x.yzx) * 0x89AB4567u;
19 | return vec3(x) / vec3(-1u);
20 | }
21 |
22 | void main() {
23 | vec2 uv = vUv;
24 | vec2 asp = uResolution / min(uResolution.x, uResolution.y);
25 |
26 | vec2 quv = uv * asp, fuv, iuv;
27 | float i, avg = 1.0;
28 | for(; i < 6.0; i++) {
29 | fuv = fract(quv);
30 | iuv = floor(quv) / pow(2.0, i) / asp;
31 | if (2.0 <= i) {
32 | // 代表点(4点)の輝度の分散を求める
33 | vec2 px = 1.0 / pow(2.0, i) / asp;
34 | px *= 0.5;
35 | vec3 c1 = texture(uSource, iuv + px * vec2(0, 0) + px * 0.5).rgb;
36 | vec3 c2 = texture(uSource, iuv + px * vec2(1, 0) + px * 0.5).rgb;
37 | vec3 c3 = texture(uSource, iuv + px * vec2(0, 1) + px * 0.5).rgb;
38 | vec3 c4 = texture(uSource, iuv + px * vec2(1, 1) + px * 0.5).rgb;
39 |
40 | vec4 g = vec4(
41 | dot(c1, vec3(0.299, 0.587, 0.114)),
42 | dot(c2, vec3(0.299, 0.587, 0.114)),
43 | dot(c3, vec3(0.299, 0.587, 0.114)),
44 | dot(c4, vec3(0.299, 0.587, 0.114))
45 | );
46 |
47 | avg = dot(vec4(1), g) / 4.0;
48 | vec4 d = g - avg;
49 |
50 | // 分散
51 | float disp = dot(vec4(1), d * d) / 4.0;
52 |
53 | if (disp < 0.001) break;
54 | }
55 | quv *= 2.0;
56 | }
57 |
58 | vec3 h = hash(vec3(iuv, i + 0.1));
59 |
60 | vec2 mapOffset;
61 | mapOffset.x = floor(avg / uMapPx.x) * uMapPx.x - uMapPx.x;
62 | mapOffset.y = floor(h.x / uMapPx.y) * uMapPx.y;
63 |
64 | // flip
65 | vec2 ruv = fuv;
66 | if (h.y < 0.3) ruv.x = 1.0 - ruv.x;
67 | if (h.y < 0.6) ruv.y = 1.0 - ruv.y;
68 |
69 | vec3 pattern = texture(uMap, mapOffset + ruv * uMapPx).rgb;
70 | if(0.95 < avg) pattern = mix(pattern, vec3(1), 0.6); // background
71 | else pattern = mix(pattern, vec3(1), 0.15);
72 |
73 | // border line
74 | vec2 auv = abs(fuv * 2.0 - 1.0);
75 | float t = 0.995 - pow(i / 3.0, 2.0) * 0.02;
76 | float b = 1.0 - step(auv.x, t) * step(auv.y, t);
77 |
78 | outColor = vec4(sat(pattern + b), 1.0);
79 | // outColor = texture(uSource, uv);
80 | }
--------------------------------------------------------------------------------
/src/scripts/core/FrameBuffer.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 |
3 | export type Options = {
4 | dpr?: number
5 | matrixAutoUpdate?: boolean
6 | size?: [number, number]
7 | }
8 |
9 | export abstract class FrameBuffer {
10 | protected readonly scene: THREE.Scene
11 | protected readonly camera: THREE.OrthographicCamera
12 | protected readonly renderTarget: THREE.WebGLRenderTarget
13 | private readonly screen: THREE.Mesh
14 |
15 | constructor(
16 | protected readonly renderer: THREE.WebGLRenderer,
17 | material: THREE.RawShaderMaterial,
18 | private options?: Options,
19 | ) {
20 | this.scene = new THREE.Scene()
21 | this.camera = new THREE.OrthographicCamera()
22 | this.renderTarget = this.createRenderTarget()
23 | this.screen = this.createScreen(material)
24 |
25 | this.setMatrixAutoUpdate(options?.matrixAutoUpdate ?? false)
26 | }
27 |
28 | private get devicePixelRatio() {
29 | return this.options?.dpr ?? this.renderer.getPixelRatio()
30 | }
31 |
32 | private setMatrixAutoUpdate(v: boolean) {
33 | this.camera.matrixAutoUpdate = v
34 | this.scene.traverse((o) => (o.matrixAutoUpdate = v))
35 | }
36 |
37 | protected get size() {
38 | const width = (this.options?.size?.[0] ?? this.renderer.domElement.width) * this.devicePixelRatio
39 | const height = (this.options?.size?.[1] ?? this.renderer.domElement.height) * this.devicePixelRatio
40 | return { width, height }
41 | }
42 |
43 | protected createRenderTarget() {
44 | const rt = new THREE.WebGLRenderTarget(this.size.width, this.size.height, {
45 | wrapS: THREE.RepeatWrapping,
46 | wrapT: THREE.RepeatWrapping,
47 | minFilter: THREE.NearestFilter,
48 | magFilter: THREE.NearestFilter,
49 | })
50 | return rt
51 | }
52 |
53 | private createScreen(material: THREE.RawShaderMaterial) {
54 | const geometry = new THREE.PlaneGeometry(2, 2)
55 | const mesh = new THREE.Mesh(geometry, material)
56 | this.scene.add(mesh)
57 | return mesh
58 | }
59 |
60 | get uniforms() {
61 | return this.screen.material.uniforms
62 | }
63 |
64 | resize() {
65 | this.renderTarget.setSize(this.size.width, this.size.height)
66 | }
67 |
68 | get texture() {
69 | return this.renderTarget.texture
70 | }
71 |
72 | render(..._args: any[]) {
73 | this.renderer.setRenderTarget(this.renderTarget)
74 | this.renderer.render(this.scene, this.camera)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/scripts/core/Three.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
3 | import Stats from 'three/examples/jsm/libs/stats.module.js'
4 |
5 | export abstract class Three {
6 | readonly renderer: THREE.WebGLRenderer
7 | readonly camera: THREE.PerspectiveCamera
8 | readonly scene: THREE.Scene
9 | private clock: THREE.Clock
10 | private _stats?: Stats
11 | private _controls?: OrbitControls
12 | readonly time = { delta: 0, elapsed: 0 }
13 |
14 | constructor(canvas: HTMLCanvasElement) {
15 | this.renderer = this.createRenderer(canvas)
16 | this.camera = this.createCamera()
17 | this.scene = this.createScene()
18 | this.clock = new THREE.Clock()
19 |
20 | window.addEventListener('resize', this._resize.bind(this))
21 | }
22 |
23 | private createRenderer(canvas: HTMLCanvasElement) {
24 | const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true })
25 | renderer.setSize(window.innerWidth, window.innerHeight)
26 | // renderer.setPixelRatio(window.devicePixelRatio)
27 | renderer.setPixelRatio(2)
28 | renderer.shadowMap.enabled = true
29 | return renderer
30 | }
31 |
32 | private createCamera() {
33 | const camera = new THREE.PerspectiveCamera(40, this.size.aspect, 0.01, 100)
34 | camera.position.z = 5
35 | return camera
36 | }
37 |
38 | private createScene() {
39 | const scene = new THREE.Scene()
40 | return scene
41 | }
42 |
43 | protected get stats() {
44 | if (!this._stats) {
45 | this._stats = new Stats()
46 | document.body.appendChild(this._stats.dom)
47 | }
48 | return this._stats
49 | }
50 |
51 | private _resize() {
52 | const { innerWidth: width, innerHeight: height } = window
53 | this.renderer.setSize(width, height)
54 | this.camera.aspect = width / height
55 | this.camera.updateProjectionMatrix()
56 | }
57 |
58 | get size() {
59 | const { width, height } = this.renderer.domElement
60 | return { width, height, aspect: width / height }
61 | }
62 |
63 | protected updateTime() {
64 | this.time.delta = this.clock.getDelta()
65 | this.time.elapsed = this.clock.getElapsedTime()
66 | }
67 |
68 | protected get controls() {
69 | if (!this._controls) {
70 | this._controls = new OrbitControls(this.camera, this.renderer.domElement)
71 | }
72 | return this._controls
73 | }
74 |
75 | protected coveredScale(imageAspect: number) {
76 | const screenAspect = this.size.aspect
77 | if (screenAspect < imageAspect) return [screenAspect / imageAspect, 1]
78 | else return [1, imageAspect / screenAspect]
79 | }
80 |
81 | protected render() {
82 | this.renderer.setRenderTarget(null)
83 | this.renderer.render(this.scene, this.camera)
84 | }
85 |
86 | dispose() {
87 | this.renderer.setAnimationLoop(null)
88 | this.renderer.dispose()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/scripts/Canvas.ts:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three'
2 | import { RawShaderMaterial } from './core/ExtendedMaterials'
3 | import { Three } from './core/Three'
4 | import quadVs from './shader/quad.vs'
5 | import effectFs from './shader/effect.fs'
6 |
7 | export class Canvas extends Three {
8 | private mainRT: THREE.WebGLRenderTarget
9 | private mainScene: THREE.Scene
10 | private effect!: THREE.Mesh
11 |
12 | constructor(canvas: HTMLCanvasElement) {
13 | super(canvas)
14 |
15 | const dpr = this.renderer.getPixelRatio()
16 | this.mainRT = new THREE.WebGLRenderTarget(this.size.width * dpr, this.size.height * dpr)
17 | this.mainScene = new THREE.Scene()
18 |
19 | this.init()
20 |
21 | this.loadAssets().then((assets) => {
22 | this.createLights()
23 | this.createModel()
24 | this.effect = this.createEffect(assets[0])
25 | window.addEventListener('resize', this.resize.bind(this))
26 | this.renderer.setAnimationLoop(this.anime.bind(this))
27 | })
28 | }
29 |
30 | private async loadAssets() {
31 | const loader = new THREE.TextureLoader()
32 |
33 | return await Promise.all(
34 | ['azulejos'].map(async (filename) => {
35 | const texture = await loader.loadAsync(`${import.meta.env.BASE_URL}textures/${filename}.webp`)
36 | texture.userData.aspect = texture.source.data.width / texture.source.data.height
37 | texture.wrapS = THREE.RepeatWrapping
38 | texture.wrapT = THREE.RepeatWrapping
39 | return texture
40 | }),
41 | )
42 | }
43 |
44 | private init() {
45 | this.mainScene.background = new THREE.Color('#fff')
46 |
47 | this.controls.enableDamping = true
48 | this.controls.dampingFactor = 0.03
49 | this.controls.enablePan = false
50 | }
51 |
52 | private createLights() {
53 | const amb = new THREE.AmbientLight('#fff', 0.5)
54 | this.mainScene.add(amb)
55 |
56 | const dir = new THREE.DirectionalLight('#fff', 2.0)
57 | dir.position.set(-1, 5, 3)
58 | this.mainScene.add(dir)
59 | }
60 |
61 | private createModel() {
62 | let geometry!: THREE.BufferGeometry
63 | const r = Math.random()
64 | if (r < 0.5) {
65 | // cylinder
66 | this.camera.position.set(0.76, 0.93, 0.92)
67 | geometry = new THREE.CylinderGeometry(0.3, 0.3, 1)
68 | geometry.rotateX(Math.PI * 0.5)
69 | } else {
70 | // torus
71 | this.camera.position.set(0.61, -0.53, 1.1)
72 | geometry = new THREE.TorusGeometry(0.3, 0.15, 48, 96)
73 | }
74 | const material = new THREE.MeshStandardMaterial({ color: '#fff', emissive: '#777' })
75 | const mesh = new THREE.Mesh(geometry, material)
76 | this.mainScene.add(mesh)
77 | }
78 |
79 | private createEffect(map: THREE.Texture) {
80 | const geometry = new THREE.PlaneGeometry(2, 2)
81 | const material = new RawShaderMaterial({
82 | uniforms: {
83 | uSource: { value: this.mainRT.texture },
84 | uResolution: { value: [this.size.width, this.size.height] },
85 | uMap: { value: map },
86 | uMapPx: { value: [1 / 6, 1 / 5] },
87 | },
88 | vertexShader: quadVs,
89 | fragmentShader: effectFs,
90 | glslVersion: '300 es',
91 | })
92 | const mesh = new THREE.Mesh(geometry, material)
93 | this.scene.add(mesh)
94 | return mesh
95 | }
96 |
97 | private resize() {
98 | const dpr = this.renderer.getPixelRatio()
99 | this.mainRT.setSize(this.size.width * dpr, this.size.height * dpr)
100 | this.effect.material.uniforms.uResolution.value = [this.size.width, this.size.height]
101 | }
102 |
103 | private anime() {
104 | this.updateTime()
105 | this.controls.update()
106 |
107 | this.renderer.setRenderTarget(this.mainRT)
108 | this.renderer.render(this.mainScene, this.camera)
109 |
110 | this.render()
111 | }
112 | }
113 |
--------------------------------------------------------------------------------