├── 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 | --------------------------------------------------------------------------------