├── .gitignore ├── .nvmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── examples ├── .gitignore ├── README.md ├── package.json ├── public │ ├── index.html │ └── readme-examples.png ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ ├── bricks.jpg │ │ ├── burger.svg │ │ ├── burning-meteor.svg │ │ ├── castle.jpg │ │ ├── castle.svg │ │ ├── city.png │ │ ├── desk.png │ │ ├── girl.png │ │ ├── moby.otf │ │ ├── parchment.png │ │ ├── wall.png │ │ ├── wall.svg │ │ └── wolf-head.svg │ ├── demos │ │ ├── Alpha.tsx │ │ ├── Blending.tsx │ │ ├── Bricks.tsx │ │ ├── Effects.tsx │ │ ├── Filters.tsx │ │ ├── Gradient.tsx │ │ ├── Images.tsx │ │ ├── Misc.tsx │ │ ├── Renders.tsx │ │ ├── Seamless.tsx │ │ ├── Shapes.tsx │ │ ├── Test.tsx │ │ └── Transformation.tsx │ ├── helpers │ │ └── getControls.ts │ ├── index.css │ ├── index.tsx │ ├── meshes │ │ ├── PrettyBox.tsx │ │ ├── PrettyCylinder.tsx │ │ ├── PrettyPlane.tsx │ │ └── PrettySphere.tsx │ ├── types │ │ ├── Demo.ts │ │ └── PrettyMesh.ts │ └── ui │ │ ├── Navigation.css │ │ └── Navigation.tsx ├── tsconfig.json └── yarn.lock ├── netlify.toml ├── package.json ├── public ├── readme-alpha.png ├── readme-blending.png ├── readme-bloom.png ├── readme-color.png ├── readme-dimensions.png ├── readme-example.png ├── readme-fill.png ├── readme-filter.png ├── readme-gradient.png ├── readme-image.png ├── readme-layer.png ├── readme-nearest.png ├── readme-outline.png ├── readme-repeat.png ├── readme-seamless.png ├── readme-shadow.png ├── readme-shapes.png ├── readme-src.png └── readme-transform.png ├── rollup.config.js ├── src ├── components │ ├── Layer.tsx │ ├── TextureSet.tsx │ ├── hooks │ │ ├── useCanvas.ts │ │ └── useTextureSet.tsx │ ├── index.ts │ └── presets │ │ └── Bricks.tsx ├── effects │ ├── alpha.ts │ ├── bloom.ts │ ├── color.ts │ ├── fill.ts │ ├── flip.ts │ ├── gradient.ts │ ├── image.ts │ ├── index.ts │ ├── nearest.ts │ ├── noise.ts │ ├── outline.ts │ ├── repeat.ts │ ├── seamless.ts │ ├── shadow.ts │ ├── shape.ts │ ├── shapes │ │ ├── circle.ts │ │ ├── curve.ts │ │ ├── index.ts │ │ ├── line.ts │ │ ├── poly.ts │ │ ├── rect.ts │ │ └── text.ts │ └── transformation.ts ├── helpers │ ├── Random.ts │ ├── flattenChildren.ts │ ├── generatePMREM.ts │ ├── mixColors.ts │ ├── newCanvasHelper.ts │ ├── rgbToHex.ts │ ├── toUUID.ts │ └── wrapText.ts ├── index.ts ├── polyfill │ └── ctx.tsx ├── setup.ts ├── storage │ └── storage.ts └── types │ ├── Layer.ts │ ├── TextureSet.ts │ └── index.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | 4 | # production 5 | /build 6 | /dist 7 | 8 | # misc 9 | .DS_Store 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | \!\!\!* -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16 -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "examples/src/temp": false, 4 | "src": false 5 | }, 6 | "search.exclude": { 7 | "examples/src/temp": false, 8 | "src": true, 9 | "dist": true, 10 | "**/build": true, 11 | "**/node_modules": true, 12 | "**/yarn-error.log": true, 13 | "**/.gitignore": true 14 | }, 15 | "search.useIgnoreFiles": false 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jorg Nieberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Texture Tinker Tool 2 | 3 | ## Table of Contents 4 | 5 | - [Texture Tinker Tool](#texture-tinker-tool) 6 | - [Table of Contents](#table-of-contents) 7 | - [Introduction](#introduction) 8 | - [Demo](#demo) 9 | - [Installation](#installation) 10 | - [Prerequisites](#prerequisites) 11 | - [Quickstart Example](#quickstart-example) 12 | - [Documentation](#documentation) 13 | - [Default Texture Settings](#default-texture-settings) 14 | - [Texture Set](#texture-set) 15 | - [Map types](#map-types) 16 | - [The _useTextureSet_ hook](#the-usetextureset-hook) 17 | - [Layer](#layer) 18 | - [Src](#src) 19 | - [Dimensions](#dimensions) 20 | - [Basic transformations](#basic-transformations) 21 | - [Fit transformations](#fit-transformations) 22 | - [Repeat](#repeat) 23 | - [Color](#color) 24 | - [Fill](#fill) 25 | - [Gradient](#gradient) 26 | - [Image smoothing\*\*](#image-smoothing) 27 | - [Shadow and glow](#shadow-and-glow) 28 | - [Outline](#outline) 29 | - [Bloom](#bloom) 30 | - [Filters](#filters) 31 | - [Blending](#blending) 32 | - [Alpha](#alpha) 33 | - [Shapes](#shapes) 34 | - [Text](#text) 35 | - [Seamless texture](#seamless-texture) 36 | - [Q and A](#q-and-a) 37 | - [Disabling the smooth effect does not always work.](#disabling-the-smooth-effect-does-not-always-work) 38 | - [Known issues and Roadmap](#known-issues-and-roadmap) 39 | - [New layer effects](#new-layer-effects) 40 | - [Technical upgrades](#technical-upgrades) 41 | - [Known bugs](#known-bugs) 42 | 43 | ## Introduction 44 | 45 | The 3D drawing tool [ThreeJS](https://threejs.org/), its React rendering solution [React Three Fiber](https://github.com/pmndrs/react-three-fiber), and on its turn the R3F abstraction library [Drei](https://github.com/pmndrs/drei) can be useful in making quick and beautiful 3D creations. And there are many other useful ThreeJS tools, helpers, libraries and documentations available around the web. However, on the level of drawing and handling _images and textures_ there is not much available, except for professional designers and experts in handling expensive photo drawing and editing programs. 46 | 47 | So I decided to create a tool where you can draw, edit, optimize and even animate your textures in an easy-to-use React environment. In a similar way popular drawing programs can handle photos and images, you can stack layers and blend them together, transform, filter and retouch them, add effects to them and draw shapes. To increase the performance, once an image, layer or texture is loaded or rendered, it is cached and reused for when the exact same input data is called a second time. 48 | 49 | The Texture Tinker Tool is a fast, powerful, elegant and dynamic texture editor, to use together with [ThreeJS](https://threejs.org/) and [React Three Fiber](https://github.com/pmndrs/react-three-fiber). It helps you to dynamically optimize your textures thanks to the many features available in the [Canvas Rendering Context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) interface, like [filters](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter), [composite operations (blending)](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation), [gradient](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient) and additional effects like [bloom](), alpha channel mapping, and outline effects. 50 | 51 | ## Demo 52 | 53 | [Texture Tinker Tool Demo](https://texture-tinker-tool.netlify.app) 54 | 55 | ## Installation 56 | 57 | ``` 58 | npm install react three @react-three/fiber react-three-texture 59 | ``` 60 | 61 | ...or... 62 | 63 | ``` 64 | yarn add react three @react-three/fiber react-three-texture 65 | ``` 66 | 67 | ## Prerequisites 68 | 69 | Before you start you need to know a bit about React, ThreeJS and React Three Fiber. If you need any additional help on these topics, please refer to the following links: 70 | 71 | - [React](https://reactjs.org/docs/getting-started.html) 72 | - [ThreeJS](https://threejs.org/) 73 | - [React Three Fiber](https://docs.pmnd.rs/react-three-fiber/getting-started/introduction) 74 | 75 | ## Quickstart Example 76 | 77 | ![Quick Example](./public/readme-example.png) 78 | 79 | ```jsx 80 | import { TextureSet, Layer } from "react-three-texture"; 81 | 82 | const ExampleBox = () => ( 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | ) 93 | ``` 94 | 95 | ## Documentation 96 | 97 | ### Default Texture Settings 98 | 99 | To apply texture settings globally and for all textures defined in the texture sets, call the texture default function at the top level of your application. All the available layer settings can be defined here. 100 | 101 | ```js 102 | textureGlobals({ 103 | dimensions: 512, 104 | filter: "contrast(85%) brightness(110%)", 105 | }); 106 | ``` 107 | 108 | ### Texture Set 109 | 110 | A texture containing a set of layers. A `map` propertly can be added to determine the type of map. Also the `dimensions` can be defined for this texture. Additionally, all features like `offset`, `repeat` and `rotation` that are normally available for the basic `Texture` object, can also be used here. Its children can only be layers. [Read more about ThreeJS Textures](https://threejs.org/docs/#api/en/textures/Texture). 111 | 112 | ```jsx 113 | ... 114 | 115 | ... 116 | 117 | ... 118 | ``` 119 | 120 | #### Map types 121 | 122 | The following map types are available. When no `map` property is defined, by default the color map is used. Technically, all `Material` properties ending with `Map` can be used as a `TextureSet` property. 123 | 124 | - `env` 125 | - `specular` 126 | - `displacement` 127 | - `normal` 128 | - `bump` 129 | - `roughness` 130 | - `metalness` 131 | - `alpha` 132 | - `light` 133 | - `emissive` 134 | - `clearcoat` 135 | - `clearcoatNormal` 136 | - `clearcoatRoughness` 137 | - `sheenRoughness` 138 | - `sheenColor` 139 | - `specularIntensity` 140 | - `specularColor` 141 | - `thickness` 142 | - `transmission` 143 | - `ao` 144 | 145 | ### The _useTextureSet_ hook 146 | 147 | Alternatively, the texture can be built by using the `useTextureSet` hook. The hook returns a `CanvasTexture` object, which can directly be used as a texture map. When applying the texture to a material, be sure to update the material, using `material.needsUpdate = true`. 148 | 149 | ```jsx 150 | const texture = useTextureSet( 151 | <> 152 | 153 | 154 | , 155 | 1024 156 | ); 157 | 158 | ; 159 | ``` 160 | 161 | | arguments | example | type | description | 162 | | ----------- | ---------------------- | ------- | -------------------------------------------------------------------------------------------------- | 163 | | layer | `` | JSX | A React Node object containing all the layers | 164 | | dimensions? | `512` | number | The width and height of the texture, in pixels | 165 | | isEnvMap? | `true` | boolean | When the type is an environment map, set this to true to be sure the PMREM is correctly generated. | 166 | 167 | ### Layer 168 | 169 | [![](./public/readme-layer.png)](./public/readme-layer.png) 170 | 171 | Layers are like a stack of transparent film paper, used in old animated movies. The bottom layer will be shown at the back, then the second layer, etcetera. All layers together will form one single texture. 172 | 173 | ```jsx 174 | // Will be drawn first and is displayed in the back 175 | 176 | 177 | // Will be drawn last and is displayed in the front 178 | ``` 179 | 180 | #### Src 181 | 182 | ![Src Example](./public/readme-src.png) 183 | 184 | Will directly load an image source onto the texture. All loaded images will be cached, to increase performance. I.e. when the same image is used somewhere else there will be no reloading. It is also possible to load SVG files, and even external sources can be used. The root folder for the source files is `/src/assets`. 185 | 186 | ```jsx 187 | 188 | 189 | 190 | 191 | 192 | ``` 193 | 194 | #### Dimensions 195 | 196 | ![Dimension Example](./public/readme-dimensions.png) 197 | 198 | You can set the width and the height (in pixels) for the layer. By default the texture dimensions are 512x512 pixels. Note, that if you wish to set the dimensions higher than the [Default Texture Settings](#default-texture-settings), you also need to increase it there. 199 | 200 | ```jsx 201 | 202 | 203 | 204 | ``` 205 | 206 | #### Basic transformations 207 | 208 | ![Transformation Example](./public/readme-transform.png) 209 | 210 | All layers can separately be moved, scaled and rotated, relative to the texture. It is also possible to flip (mirror) the layer either horizontally or vertically 211 | 212 | ```jsx 213 | 214 | 215 | 216 | 217 | 218 | ``` 219 | 220 | | property | default value | arguments | description | 221 | | -------- | ------------- | --------- | ------------------------------------------------------------------------------------------------------ | 222 | | position | `[0, 0]` | x, y | The position of the layer. | 223 | | scale | `[1, 1]` | w, h | The scale of the layer. | 224 | | rotation | `0` | rad | The rotation of the layer in radians. The rotation will be done relative to the center of the texture. | 225 | | flipX | `false` | boolean | Flip the layer horizontally. | 226 | | flipY | `false` | boolean | Flip the layer vertically. | 227 | 228 | #### Fit transformations 229 | 230 | ![Image Example](./public/readme-image.png) 231 | 232 | Layers and images that are smaller or larger than the layer can be fit into the layer as we like, without getting deformed. We can choose to fit a rectangular sized image on a square sized texture, while keeping the aspect ratio of the image. Also, when the image is too big to fit on the texture we can choose to position the image to a side. 233 | 234 | ```jsx 235 | // "size-max center middle" 236 | 237 | 238 | 239 | 240 | ``` 241 | 242 | | string | description | 243 | | -------------- | --------------------------------------------------------------------------------------------------------------------------------- | 244 | | size-fill \*\* | Image will be resized to match the given dimension, and may stretch it to fit. | 245 | | size-max \* | Will fit the image to whichever side (width or height) is smaller, and keeping the image's aspect ratio. This may clip the image. | 246 | | size-min | Will fit the image to whichever side (width or height) is larger, and keeping the image's aspect ratio. | 247 | | size-x | Will fit the image to its width, while keeping the image's aspect ratio. | 248 | | size-y | Will fit the image to its height, while keeping the image's aspect ratio. | 249 | | size-none | Will not fit the image and will keep its original size. | 250 | | top \*\* | Will align the image to the top. | 251 | | right | Will align the image to the right. | 252 | | bottom \*\* | Will align the image to the bottom. | 253 | | left | Will align the image to the left. | 254 | | center \* | Will horizontally align the image to the center. | 255 | | middle \* | Will vertically align the image to the middle. | 256 | 257 | \* default, when defining the property "image" without a value 258 | 259 | \*\* default, when not defining the property "image" 260 | 261 | #### Repeat 262 | 263 | ![Repeat Example](./public/readme-repeat.png) 264 | 265 | Instead of a single render of a layer, it is also possible to repeat it as a pattern. When scaling the layer down, the pattern will become visible. 266 | 267 | ```jsx 268 | 269 | ``` 270 | 271 | #### Color 272 | 273 | ![Outline Example](./public/readme-color.png) 274 | 275 | Overwrite the colors in your layers with a single color. Useful to colorize your monochrome SVG images. When setting the alpha channel lower than 100% it will blend the original colors with the overwritten color. 276 | 277 | ```jsx 278 | // "white" 279 | 280 | 281 | 282 | 283 | 284 | 285 | ``` 286 | 287 | #### Fill 288 | 289 | ![Fill Example](./public/readme-fill.png) 290 | 291 | Will fill the layer with a single color. The alpha channel can also be used to make the fill color (semi-)transparent. 292 | 293 | ```jsx 294 | // "black" 295 | 296 | 297 | 298 | 299 | 300 | 301 | ``` 302 | 303 | #### Gradient 304 | 305 | ![Gradient Example](./public/readme-gradient.png) 306 | 307 | Gradients is an operation to smoothly blend two or more colors into each other. See the [linear gradient page](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createLinearGradient) and the [radial gradient page](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createRadialGradient) of the MDN web docs for more information. 308 | 309 | ```jsx 310 | // { type: "linear", from: [0, 0], to: [0, 1], stops: [[0, "white"], [1, "black"]] } 311 | 312 | 313 | 314 | 326 | ``` 327 | 328 | | key | default value | arguments | description | 329 | | ----- | ------------------------------ | ---------------- | ----------------------------------------------------------------------------------------------------- | 330 | | type | `"linear"` | linear or radial | the type of gradient. Can be linear or radial. | 331 | | from | `[0, 0]` or `[0.5, 0.5, 0]` | x, y, r? | The starting point of the gradient. The radius `r` only applies for radial gradients. | 332 | | to | `[0, 1]` or `[0.5, 0.5, 1]` | x, y, r? | The end point of the gradient. The radius `r` only applies for radial gradients. | 333 | | stops | `[[0, "white"], [1, "black"]]` | index, color | The stop index and color between the start and the end point. Multiply stops and indexes can be used. | 334 | 335 | #### Image smoothing\*\* 336 | 337 | ![Image smoothing Example](./public/readme-nearest.png) 338 | 339 | Turn off image smoothing for the layer (the "nearest neighbour" algorithm), when scaling/fitting the image. See the [image smoothing page](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled) of the MDN web docs for more information. 340 | 341 | ```jsx 342 | // true 343 | ``` 344 | 345 | _\*\* Unfortunately, currently image smoothing is not working in most of the browsers_ 346 | 347 | #### Shadow and glow 348 | 349 | ![Shadow and glow Example](./public/readme-shadow.png) 350 | 351 | Apply a shadow (or glow) effect to the layer. See the [shadow page](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/shadowBlur) of the MDN web docs for more information. 352 | 353 | ```jsx 354 | // { color: "black", blur: 20, offset: [0, 0] } 355 | 356 | 357 | ``` 358 | 359 | | key | default value | arguments | description | 360 | | ------ | ------------- | --------- | -------------------------------------------------- | 361 | | color | `"black"` | | The color of the effect | 362 | | blur | `20` | 0 to x | The blur radius of the effect | 363 | | offset | `[0, 0]` | x, y | The position offset from the center of the effect. | 364 | 365 | #### Outline 366 | 367 | ![Outline Example](./public/readme-outline.png) 368 | 369 | You can give your image an outline effect. Other than shadow or glow, outline will not be smooth and does not have an offset. 370 | 371 | ```jsx 372 | // { color: "black", size: 1, detail: 8 } 373 | 374 | 375 | ``` 376 | 377 | | key | default value | arguments | description | 378 | | ------- | ------------- | --------- | -------------------------------------------------------------------------------------------- | 379 | | color | `"black"` | | The color of the outline. | 380 | | size | `1` | 1 to x | The thickness of the outline. | 381 | | quality | `8` | 1 to x | The detail of the outline effect. The higher the more instances of the effect will be drawn. | 382 | 383 | #### Bloom 384 | 385 | ![Outline Example](./public/readme-bloom.png) 386 | 387 | Bloom is an effect, well known to photographers and videogame developers, to intensify the light parts of an image with a bleeding effect of brightness. 388 | 389 | ```jsx 390 | // { size: 30, strength: 0.4, softness: 0.7, detail: 10, darken: false } 391 | 392 | 393 | 394 | 395 | 396 | ``` 397 | 398 | | key | default value | arguments | description | 399 | | -------- | ------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------- | 400 | | size | `30` | 0 to x | Indicates the radius of the bloom effect. The higher the larger the scale of the effect. | 401 | | strength | `0.4` | 0.0 to 1.0 | The power of the effect. The higher the value the brighter the effect will remain, moving further away from the center. | 402 | | softness | `0.7` | 0.0 to 1.0 | The fallout of the effect. The higher the value the faster the effect will fade, moving further away from the center. | 403 | | detail | `10` | 1 to x | The detail of the bloom effect. The higher the more instances of the effect will be drawn. | 404 | | darken | `false` | boolean | Optionally darken the effect. | 405 | 406 | #### Filters 407 | 408 | ![Filters Example](./public/readme-filter.png) 409 | 410 | You can change brightness, contrast, saturation, blur, and many other filters. See the [filter page](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter) of the MDN web docs for more information. 411 | 412 | ```jsx 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | ``` 421 | 422 | #### Blending 423 | 424 | ![Blending Example](./public/readme-blending.png) 425 | 426 | Blending is a method to mix the colors of two or more layers together by a mathematic operation. Also it is possible to change the transparency by color. See the [composition operation page](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation) of the MDN web docs for more information. Blending only has effect with two or more layers. 427 | 428 | ```jsx 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | ``` 438 | 439 | #### Alpha 440 | 441 | ![Alpha Example](./public/readme-alpha.png) 442 | 443 | The alpha (transparency) channel can be altered in many ways. It is possible to alter the alpha channel in such a way that darker colors become more transparent. Or vice versa. 444 | 445 | ```jsx 446 | // level: 0.5 447 | 448 | // { level: 1, power: 1, offset: 0, reverse: false } 449 | 450 | 451 | 452 | 453 | ``` 454 | 455 | | key | default value | arguments | description | 456 | | ------- | ------------- | --------- | ----------------------------------------------------------------------------------------------- | 457 | | level | `1` | 0 to 1 | The opacity of the layer. 0 means transparent, 1 means opaque. | 458 | | power | `1` | 0 to x | The power of the alpha map levels. Higher power means more difference between the alpha levels. | 459 | | offset | `0` | 0 to 1 | Shift the alpha levels down or up, making respectively less or more colors transparent. | 460 | | reverse | `false` | boolean | Reverse the alpha channels to make lighter colors more transparent. | 461 | 462 | #### Shapes 463 | 464 | ![Shapes Example](./public/readme-shapes.png) 465 | 466 | A variety of basic shapes can be drawn on a layer. Think of lines, curved lines, circles or rectanges. 467 | 468 | ```jsx 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | ``` 477 | 478 | | property | default value | arguments | description | 479 | | -------------- | ------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 480 | | shapeThickness | `0` | 0 to x | The thickness of the shape. Will work with all shapes. A shape thickness of 0 will fill the shape. | 481 | | shapeRounded | `false` | boolean | Will make lines rounded in the corners, and lines and curved lines rounded on the start and end points. | 482 | | line | | x1, y1, x2, y2, ... | Draw a line from one coordinate to the next. Ending with the same point as the starting point will close the path. | 483 | | curved | | x1, y1, x2, y2, x3, y3, x4?, y4? | Draw a curved line from one coordinate to the next. Can be a bezier curve (3 points), or a quadratic curve (4 points). | 484 | | circle | | x, y, rx, ry?, rotation?, start?, end?, cc? | Will draw a circle at a certain point `x, y` and a radius of `rx`. When a second radius `ry` is defined the circle will become an ellipse, and with the start and end point defined the circle is only partly drawn. Optionally the circle can be drawn counter-clockwise with `cc`. | 485 | | rect | | x, y, w, h?, rounded? | Will draw a rectangle at a certain point `x, y` and a fixed size of `w` and and optional height of `h`. It is also possible to give the rectangle rounded corners. | 486 | 487 | #### Text 488 | 489 | Texts can be created in all sorts of types, styles, colors and sizes. Font faces can also be retreived from files or a url. The property `shapeThickness` can also be used here, to create outlined text. 490 | 491 | ```jsx 492 | 493 | 494 | 495 | 496 | 507 | ``` 508 | 509 | | key | default value | arguments | description | 510 | | ------ | ------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 511 | | value | | string | The text value. | 512 | | font | `serif` | string | The font family of the text. System fonts can be used, but also a path to a custom font file, or even a url can be used to retreive a font face. The root folder for the source files is `/src/assets`. | 513 | | style | | string | The font style. For example `italic` or `oblique`. | 514 | | weight | | string | The font weight. Examples are `bold` or `lighter`, but you can use number values to define the strength of the weight. | 515 | | width | `0` | number | The maximum width of the text. When the text exceeds this width, the words will wrap down to the next line. The value is a factor of the full width of the layer. A value of 0 will not wrap the text. | 516 | | height | `1.3` | number | The line height of the text. The number is a multiplier for the height of the font. | 517 | | align | `center` | left, center of right | The horizontal alignment of the text. The text will orientate around the position of the layer. | 518 | | base | `middle` | top, middle or bottom | The vertical alignment of the text. The text will orientate around the position of the layer. | 519 | 520 | #### Seamless texture 521 | 522 | ![Seamless Example](./public/readme-seamless.png) 523 | 524 | Seamless textures are textures that can be repeated without leaving an ugly "seam" between the copies of the image. Finding a well created seamless image on the internet can be either expensive or difficult. And manually adjusting an image in a photo editing tool can sometimes be a time-consuming and difficult job. Here, without much effort it is possible to make a texture seamless. 525 | 526 | ```jsx 527 | // 528 | ``` 529 | 530 | | key | default value | arguments | description | 531 | | ------------ | ------------- | --------- | ------------------------------------------------------------------------------------------------------------- | 532 | | offset | `[0.3, 0.3]` | 0 to 1 | The offset of the seamless edges. | 533 | | size | `[0.2, 0.2]` | 0 to 0.5 | The size of the seamless edges. | 534 | | both | `false` | boolean | Both edges (x and y) are affected by the offset. | 535 | | flipX | `false` | boolean | Flip the seamless edge horizontally. | 536 | | flipY | `false` | boolean | Flip the seamless edge vertically. | 537 | | alphaOffset | `0` | 0 to 1 | The edges are affected by an alpha channel offset. Is disabled when the value is 0. See also [alpha](#alpha). | 538 | | alphaReverse | `false` | boolean | When alphaOffset is larger than 0, set this to reverse the alpha channel. See also [alpha](#alpha). | 539 | 540 | ## Q and A 541 | 542 | ### Disabling the smooth effect does not always work. 543 | 544 | When using the `nearest` property, the texture's smooth effect is not always disabled. Then try to use the ThreeJS `texture.magFilter = THREE.NearestFilter` value instead. 545 | 546 | ## Known issues and Roadmap 547 | 548 | ### New layer effects 549 | 550 | - [ ] Flip layer (x, y, both) 551 | - [x] Repeat layer and pattern drawing 552 | - [x] Drawing shapes and text 553 | - [ ] Inner shadow/glow 554 | - [ ] Bevel and emboss effect 555 | - [ ] Image scaling while maintaining aspect ratio 556 | - [ ] Seamless rendering 557 | - [ ] Procedural rendering 558 | - [ ] clouds 559 | - [ ] perlin noise 560 | - [ ] distortion 561 | - [ ] walls 562 | - [ ] Sharpen effect 563 | - [x] Initial setup to add more sizes than 512x512 564 | 565 | ### Technical upgrades 566 | 567 | - [x] Migration to Typescript 568 | - [ ] Adding JSDoc 569 | - [ ] Use "offscreenCanvas" for better performance 570 | - [ ] Layer groups 571 | 572 | ## Known bugs 573 | 574 | - [x] Cached large images should keep their original size, instead of scaling them down to 512x512. 575 | 576 | --- 577 | 578 | - [ ] _to do_ 579 | - [x] _completed_ 580 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # production 7 | /build 8 | /dist 9 | 10 | # misc 11 | .DS_Store 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | react-three-texture.tgz 22 | /src/temp -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Texture Tinker Tool - Example Demo 2 | 3 | ![Example Demo](./public/readme-examples.png) 4 | 5 | ## Introduction 6 | 7 | This is an examples demo of the Texture Tinker Tool. Go to https://github.com/jnieberg/react-three-texture for more information about this package. 8 | 9 | ## Installation and Run 10 | 11 | ### Yarn 12 | 13 | ``` 14 | git clone https://github.com/jnieberg/react-three-texture.git 15 | cd react-three-texture/examples 16 | yarn 17 | yarn start 18 | ``` 19 | 20 | ### NPM 21 | 22 | ``` 23 | git clone https://github.com/jnieberg/react-three-texture.git 24 | cd react-three-texture/examples 25 | npm install 26 | npm start 27 | ``` 28 | 29 | ## To do 30 | 31 | - Full freeform demo 32 | - Source code preview 33 | - Copy source code to clipboard 34 | - Fix shadow glitch 35 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "texture-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-three/drei": "^9.88.11", 7 | "@react-three/fiber": "^8.15.10", 8 | "@testing-library/jest-dom": "^5.16.5", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@types/react": "^18.2.37", 12 | "leva": "^0.9.35", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-scripts": "^5.0.1", 16 | "react-three-texture": "*", 17 | "three": "^0.158.0", 18 | "typescript": "^4.8.2", 19 | "web-vitals": "^2.1.4", 20 | "wouter": "^2.8.0-alpha.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "npm-run-all build:texture build:demo", 25 | "build:texture": "yarn add react-three-texture@latest", 26 | "build:demo": "INLINE_RUNTIME_CHUNK=false react-scripts build" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "@types/three": "^0.158.2", 48 | "npm-run-all": "^4.1.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Texture Tinker Tool 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/public/readme-examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/public/readme-examples.png -------------------------------------------------------------------------------- /examples/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | position: absolute; 3 | width: 100% !important; 4 | height: 100% !important; 5 | } 6 | 7 | #textureset__preview { 8 | position: absolute; 9 | bottom: 0; 10 | left: 0; 11 | display: none; 12 | pointer-events: none; 13 | } 14 | 15 | #textureset__preview > .texture { 16 | height: 136px; 17 | bottom: 72px; 18 | overflow-x: auto; 19 | } 20 | #textureset__preview > .texture > canvas { 21 | height: 128px; 22 | margin: 4px 4px 0; 23 | } 24 | 25 | #textureset__preview > .layer { 26 | height: 72px; 27 | bottom: 0; 28 | overflow-x: auto; 29 | } 30 | #textureset__preview > .layer > canvas { 31 | height: 64px; 32 | margin: 4px 4px 0; 33 | } 34 | 35 | #textureset__preview.show { 36 | display: block !important; 37 | } 38 | -------------------------------------------------------------------------------- /examples/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Canvas, invalidate, useFrame, useThree } from "@react-three/fiber"; 3 | import { ACESFilmicToneMapping, SpotLight, SRGBColorSpace, Vector3, VSMShadowMap } from "three"; 4 | import { Suspense, useEffect, useRef } from "react"; 5 | import { Environment, OrbitControls, Plane } from "@react-three/drei"; 6 | import { Navigation, Pages } from "./ui/Navigation"; 7 | 8 | const SceneSetup = () => { 9 | const { gl } = useThree(); 10 | 11 | const floorColor = "#668877"; 12 | const backColor = "#bbccdd"; 13 | 14 | useEffect(() => { 15 | gl.shadowMap.enabled = true; 16 | gl.shadowMap.type = VSMShadowMap; 17 | gl.shadowMap.needsUpdate = true; 18 | invalidate(); 19 | }, [gl.shadowMap]); 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 26 | 27 | 28 | 29 | new Vector3(0, 0, 0)} 36 | color="white" 37 | penumbra={1} 38 | angle={Math.PI * 0.3} 39 | decay={2} 40 | shadow-bias={-0.001} 41 | shadow-mapSize-width={1024} 42 | shadow-mapSize-height={1024} 43 | shadow-camera-near={0.1} 44 | shadow-camera-far={40} 45 | shadow-radius={4} 46 | shadow-blurSamples={20} 47 | /> 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | const Controls = () => { 55 | const light = useRef(null); 56 | const { camera } = useThree(); 57 | 58 | useFrame(() => { 59 | light.current?.position.copy(camera.position); 60 | }); 61 | 62 | return ( 63 | <> 64 | new Vector3(0, 0, 0)} 70 | penumbra={0.5} 71 | angle={Math.PI * 0.1} 72 | decay={2} 73 | /> 74 | 75 | 76 | ); 77 | }; 78 | 79 | const App = () => ( 80 | <> 81 | 82 | 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | 104 | export default App; 105 | -------------------------------------------------------------------------------- /examples/src/assets/bricks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/bricks.jpg -------------------------------------------------------------------------------- /examples/src/assets/burger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | 23 | 24 | 25 | 29 | 30 | 31 | 36 | 37 | 38 | 43 | 44 | 45 | 49 | 50 | 51 | 56 | 57 | 58 | 63 | 64 | 65 | 70 | 71 | 72 | 77 | 78 | 79 | 84 | 85 | 86 | 92 | 93 | 94 | 99 | 100 | 101 | 106 | 107 | 108 | 114 | 115 | 116 | 123 | 124 | 125 | 131 | 132 | 133 | 138 | 139 | 140 | 145 | 146 | 147 | 152 | 153 | 154 | 159 | 160 | 161 | 165 | 166 | 167 | 171 | 172 | 173 | 177 | 178 | 179 | 183 | 184 | 185 | 189 | 190 | 191 | 195 | 196 | 197 | 203 | 204 | 205 | 209 | 210 | 211 | 215 | 216 | 217 | 227 | 228 | 229 | 235 | 236 | 237 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /examples/src/assets/burning-meteor.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/assets/castle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/castle.jpg -------------------------------------------------------------------------------- /examples/src/assets/castle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/assets/city.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/city.png -------------------------------------------------------------------------------- /examples/src/assets/desk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/desk.png -------------------------------------------------------------------------------- /examples/src/assets/girl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/girl.png -------------------------------------------------------------------------------- /examples/src/assets/moby.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/moby.otf -------------------------------------------------------------------------------- /examples/src/assets/parchment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/parchment.png -------------------------------------------------------------------------------- /examples/src/assets/wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/examples/src/assets/wall.png -------------------------------------------------------------------------------- /examples/src/assets/wall.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/assets/wolf-head.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/src/demos/Alpha.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | 6 | const Alpha: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { level, power, offset, reverse } = useControls(props); 9 | 10 | return Mesh ? ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) : null; 31 | }; 32 | 33 | export default Alpha; 34 | -------------------------------------------------------------------------------- /examples/src/demos/Blending.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import DemoProps from "../types/Demo"; 3 | import { FC } from "react"; 4 | import { Layer, TextureSet } from "react-three-texture"; 5 | 6 | const Blending: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { "compositing operation": blending } = useControls(props); 9 | 10 | return Mesh ? ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 | 45 | 46 | 47 | 48 | 49 | 50 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ) : null; 68 | }; 69 | 70 | export default Blending; 71 | -------------------------------------------------------------------------------- /examples/src/demos/Bricks.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, MapType, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | import Bricks from "../temp/components/presets/Bricks"; 6 | 7 | const Presets: FC = ({ globalProps, ...props }) => { 8 | const { mesh: Mesh } = globalProps; 9 | const { width, height, thickness, radius, offsetX, randomizeX, randomizeY, randomizeS, "random seed": seed } = useControls(props); 10 | 11 | const PrettyBricks = ({ map }: { map?: MapType }) => ( 12 | 13 | 14 | 25 | {map && } 26 | 27 | ); 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default Presets; 39 | -------------------------------------------------------------------------------- /examples/src/demos/Effects.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | 6 | const Effects: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { size, strength, softness, detail, darken, blur, color, offset, colorO, sizeO, detailO } = useControls(props); 9 | 10 | return Mesh ? ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 37 | 38 | 39 | 40 | ) : null; 41 | }; 42 | 43 | export default Effects; 44 | -------------------------------------------------------------------------------- /examples/src/demos/Filters.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | 6 | const Filters: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { brightness, contrast, blur, hue, saturate, invert } = useControls(props); 9 | const filters = `brightness(${brightness}%) contrast(${contrast}%) blur(${blur}px) hue-rotate(${hue}deg) saturate(${saturate}%) invert(${invert})`; 10 | return Mesh ? ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) : null; 29 | }; 30 | 31 | export default Filters; 32 | -------------------------------------------------------------------------------- /examples/src/demos/Gradient.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | 6 | const Gradient: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { color1, color2, color3, from, to, fromRad, toRad } = useControls(props); 9 | 10 | return Mesh ? ( 11 | <> 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 35 | 36 | 37 | 38 | 39 | 51 | 52 | 53 | 54 | ) : null; 55 | }; 56 | 57 | export default Gradient; 58 | -------------------------------------------------------------------------------- /examples/src/demos/Images.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | 6 | const Images: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { url, upload, "image fit": fit, "image horizontal alignment": alignX, "image vertical alignment": alignY } = useControls(props); 9 | 10 | return Mesh ? ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) : null; 34 | }; 35 | 36 | export default Images; 37 | -------------------------------------------------------------------------------- /examples/src/demos/Misc.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useRef, useState } from "react"; 2 | import DemoProps from "../types/Demo"; 3 | import { useCanvas } from "../temp/components/hooks/useCanvas"; 4 | import Bricks from "../temp/components/presets/Bricks"; 5 | import { Layer } from "react-three-texture"; 6 | import { CanvasTexture, MeshStandardMaterial } from "three"; 7 | 8 | const Misc: FC = () => { 9 | const [texture, setTexture] = useState(); 10 | const ref = useRef(null); 11 | const canvas = useCanvas( 12 | <> 13 | 14 | 15 | 16 | 17 | , 18 | 512 19 | ); 20 | 21 | useEffect(() => { 22 | if (canvas) { 23 | setTexture(new CanvasTexture(canvas)); 24 | if (ref.current) ref.current.needsUpdate = true; 25 | } 26 | }, [canvas]); 27 | 28 | return ( 29 | 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export default Misc; 37 | -------------------------------------------------------------------------------- /examples/src/demos/Renders.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | 6 | const Renders: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { "random seed": randomSeed, "perlin seed": perlinSeed, "perlin detail": detail } = useControls(props); 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default Renders; 25 | -------------------------------------------------------------------------------- /examples/src/demos/Seamless.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { RepeatWrapping } from "three"; 4 | import { Layer, TextureSet } from "react-three-texture"; 5 | import DemoProps from "../types/Demo"; 6 | 7 | const Seamless: FC = ({ globalProps, ...props }) => { 8 | const { mesh: Mesh } = globalProps; 9 | const { url, offsetX, sizeX, offsetY, sizeY, "offset both": both, alphaOffset, alphaReverse, flipX, flipY } = useControls(props); 10 | return Mesh ? ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) : null; 19 | }; 20 | 21 | export default Seamless; 22 | -------------------------------------------------------------------------------- /examples/src/demos/Shapes.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Vec2 } from "three"; 4 | import { Layer, MapType, TextureSet } from "react-three-texture"; 5 | import DemoProps from "../types/Demo"; 6 | 7 | const ShapeTexture = ({ map, p1, p2, p3, p4 }: { map?: MapType; p1: Vec2; p2: Vec2; p3: Vec2; p4: Vec2 }) => { 8 | const props = { color: "black", outline: map ? false : { color: "#88ff00", size: 10, detail: 20 } }; 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | const TextTexture = ({ 21 | map, 22 | value, 23 | scale, 24 | bold, 25 | italic, 26 | alignment, 27 | }: { 28 | map?: MapType; 29 | value: string; 30 | scale: number; 31 | bold: boolean; 32 | italic: boolean; 33 | alignment: boolean; 34 | }) => { 35 | const textProps = { 36 | value, 37 | width: 0.8, 38 | weight: bold ? "bold" : "", 39 | style: italic ? "italic" : "", 40 | }; 41 | 42 | return ( 43 | 44 | 45 | 56 | {!map ? ( 57 | 69 | ) : null} 70 | 71 | ); 72 | }; 73 | 74 | const RectTexture = ({ map, radius }: { map?: MapType; radius: number }) => ( 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | 82 | const Shapes: FC = ({ globalProps, ...props }) => { 83 | const { positionA, positionB, positionC, positionD, value, scale, bold, italic, alignment, radius } = useControls(props); 84 | const { mesh: Mesh } = globalProps; 85 | 86 | return Mesh ? ( 87 | <> 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | ) : null; 102 | }; 103 | 104 | export default Shapes; 105 | -------------------------------------------------------------------------------- /examples/src/demos/Test.tsx: -------------------------------------------------------------------------------- 1 | import { invalidate, useFrame } from "@react-three/fiber"; 2 | import { FC, useEffect, useRef } from "react"; 3 | import { TextureSetProps } from "react-three-texture"; 4 | import { MeshStandardMaterial, RepeatWrapping } from "three"; 5 | import PrettyBox from "../meshes/PrettyBox"; 6 | import PrettySphere from "../meshes/PrettySphere"; 7 | import { MapType, Layer, TextureSet, useTextureSet } from "react-three-texture"; 8 | import DemoProps from "../types/Demo"; 9 | 10 | const LayerHelp = { 11 | Map: ({ color, fill }: { color: string; fill: string }) => ( 12 | <> 13 | 14 | 25 | 26 | ), 27 | }; 28 | 29 | const Test: FC = () => { 30 | const refCube = useRef(null); 31 | const ref = useRef(null); 32 | const textureRef = useRef(null); 33 | let offsetIncrement = 0; 34 | 35 | useFrame(() => { 36 | if (refCube.current && textureRef.current) { 37 | refCube.current.rotateY(0.002); 38 | textureRef.current.offset.set(offsetIncrement, 0); 39 | offsetIncrement += 0.01; 40 | invalidate(); 41 | } 42 | }); 43 | 44 | const ExtraLayer = () => ; 45 | 46 | const texture = useTextureSet( 47 | <> 48 | 49 | 50 | 51 | , 52 | 64 53 | ); 54 | 55 | const envTexture = useTextureSet(, 2048, true); 56 | 57 | const children = ( 58 | <> 59 | 60 | 61 | 62 | ); 63 | 64 | const TestTextureSet = ({ map }: { map?: MapType }) => ( 65 | 66 | 67 | {children} 68 | 69 | ); 70 | 71 | useEffect(() => { 72 | if (ref.current) ref.current.needsUpdate = true; 73 | }, [texture]); 74 | 75 | return ( 76 | <> 77 | 78 | 79 | 80 | {children} 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | ); 116 | }; 117 | 118 | export default Test; 119 | -------------------------------------------------------------------------------- /examples/src/demos/Transformation.tsx: -------------------------------------------------------------------------------- 1 | import { useControls } from "leva"; 2 | import { FC } from "react"; 3 | import { Layer, LayerProps, TextureSet } from "react-three-texture"; 4 | import DemoProps from "../types/Demo"; 5 | 6 | const Transformation: FC = ({ globalProps, ...props }) => { 7 | const { mesh: Mesh } = globalProps; 8 | const { position, scale, rotation, flipX, flipY, repeat } = useControls(props); 9 | const centerX = 0.15 - scale.x * 0.15; 10 | const centerY = 0.15 - scale.y * 0.15; 11 | const greenBoxProps: LayerProps = { position: [position.x, position.y], rotation, fit: "center middle" }; 12 | const rubicProps: LayerProps = { scale: [scale.x * 0.3, scale.y * 0.3], rotation, blend: "lighten" }; 13 | 14 | return Mesh ? ( 15 | <> 16 | 17 | 18 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ) : null; 57 | }; 58 | 59 | export default Transformation; 60 | -------------------------------------------------------------------------------- /examples/src/helpers/getControls.ts: -------------------------------------------------------------------------------- 1 | import { folder } from "leva"; 2 | 3 | type LevaControls = Record; 4 | 5 | const getControls = (location: string) => { 6 | let controls: LevaControls = {}; 7 | 8 | switch (location) { 9 | case "/images": 10 | controls = { 11 | "Image source": folder({ 12 | upload: { 13 | image: "girl.png", 14 | }, 15 | url: "https://images.unsplash.com/photo-1581373449483-37449f962b6c", 16 | }), 17 | "Image positioning": folder({ 18 | "image fit": { 19 | options: { 20 | max: "size-max", 21 | min: "size-min", 22 | x: "size-x", 23 | y: "size-y", 24 | fill: "size-fill", 25 | none: "size-none", 26 | }, 27 | }, 28 | "image horizontal alignment": { 29 | options: { 30 | center: "center", 31 | left: "left", 32 | right: "right", 33 | }, 34 | }, 35 | "image vertical alignment": { 36 | options: { 37 | middle: "middle", 38 | top: "top", 39 | bottom: "bottom", 40 | }, 41 | }, 42 | }), 43 | }; 44 | break; 45 | case "/transformation": 46 | controls = { 47 | Transformation: folder({ 48 | position: { 49 | value: { x: 0, y: 0 }, 50 | step: 0.01, 51 | }, 52 | scale: { 53 | value: { x: 1, y: 1 }, 54 | step: 0.01, 55 | }, 56 | rotation: { 57 | value: 0, 58 | min: 0, 59 | max: Math.PI * 2, 60 | step: 0.1, 61 | }, 62 | }), 63 | Extra: folder({ 64 | flipX: false, 65 | flipY: false, 66 | repeat: false, 67 | }), 68 | }; 69 | break; 70 | case "/gradient": 71 | controls = { 72 | Colors: folder({ 73 | color1: "#00ffff", 74 | color2: "#ff00ff", 75 | color3: "#ffff00", 76 | }), 77 | "Gradient Points": folder({ 78 | from: { 79 | value: { x: 0.3, y: 0.3 }, 80 | x: { 81 | min: 0, 82 | max: 1, 83 | step: 0.01, 84 | }, 85 | y: { 86 | min: 0, 87 | max: 1, 88 | step: 0.01, 89 | }, 90 | }, 91 | to: { 92 | value: { x: 0.7, y: 0.7 }, 93 | x: { 94 | min: 0, 95 | max: 1, 96 | step: 0.01, 97 | }, 98 | y: { 99 | min: 0, 100 | max: 1, 101 | step: 0.01, 102 | }, 103 | }, 104 | }), 105 | "Radial Gradient": folder({ 106 | fromRad: { 107 | value: 0, 108 | min: 0, 109 | max: 1, 110 | step: 0.01, 111 | }, 112 | toRad: { 113 | value: 0.7, 114 | min: 0, 115 | max: 1, 116 | step: 0.01, 117 | }, 118 | }), 119 | }; 120 | break; 121 | case "/blending": 122 | controls = { 123 | Blending: folder({ 124 | "compositing operation": { 125 | options: { 126 | "source over": "source-over", 127 | "source in": "source-in", 128 | "source out": "source-out", 129 | "source atop": "source-atop", 130 | "destination over": "destination-over", 131 | "destination in": "destination-in", 132 | "destination out": "destination-out", 133 | "destination atop": "destination-atop", 134 | copy: "copy", 135 | xor: "xor", 136 | lighter: "lighter", 137 | multiply: "multiply", 138 | screen: "screen", 139 | overlay: "overlay", 140 | lighten: "lighten", 141 | darken: "darken", 142 | "color dodge": "color-dodge", 143 | "color burn": "color-burn", 144 | "hard light": "hard-light", 145 | "soft light": "soft-light", 146 | difference: "difference", 147 | exclusion: "exclusion", 148 | hue: "hue", 149 | saturation: "saturation", 150 | color: "color", 151 | luminosity: "luminosity", 152 | }, 153 | }, 154 | }), 155 | }; 156 | break; 157 | case "/filters": 158 | controls = { 159 | Filters: folder({ 160 | brightness: { 161 | value: 100, 162 | min: 0, 163 | max: 400, 164 | step: 10, 165 | }, 166 | contrast: { 167 | value: 100, 168 | min: 0, 169 | max: 400, 170 | step: 10, 171 | }, 172 | hue: { 173 | value: 0, 174 | min: 0, 175 | max: 360, 176 | step: 10, 177 | }, 178 | saturate: { 179 | value: 100, 180 | min: 0, 181 | max: 400, 182 | step: 10, 183 | }, 184 | invert: { 185 | value: 0, 186 | min: 0, 187 | max: 1, 188 | step: 0.05, 189 | }, 190 | blur: { 191 | value: 0, 192 | min: 0, 193 | max: 20, 194 | step: 1, 195 | }, 196 | }), 197 | }; 198 | break; 199 | case "/effects": 200 | controls = { 201 | Bloom: folder({ 202 | size: { 203 | value: 30, 204 | min: 0, 205 | max: 50, 206 | step: 1, 207 | }, 208 | strength: { 209 | value: 0.4, 210 | min: 0, 211 | max: 1, 212 | step: 0.05, 213 | }, 214 | softness: { 215 | value: 0.7, 216 | min: 0, 217 | max: 1, 218 | step: 0.05, 219 | }, 220 | detail: { 221 | value: 10, 222 | min: 1, 223 | max: 20, 224 | step: 1, 225 | }, 226 | darken: false, 227 | }), 228 | Shadow: folder({ 229 | color: "#440000", 230 | blur: { 231 | value: 10, 232 | min: 0, 233 | max: 40, 234 | step: 2, 235 | }, 236 | offset: { 237 | value: { x: 5, y: 5 }, 238 | step: 1, 239 | }, 240 | }), 241 | Outline: folder({ 242 | colorO: "#ffffff", 243 | sizeO: { 244 | value: 4, 245 | min: 0, 246 | max: 20, 247 | step: 1, 248 | }, 249 | detailO: { 250 | value: 10, 251 | min: 1, 252 | max: 20, 253 | step: 1, 254 | }, 255 | }), 256 | }; 257 | break; 258 | case "/alpha": 259 | controls = { 260 | Alpha: folder({ 261 | level: { 262 | value: 1, 263 | min: 0, 264 | max: 1, 265 | step: 0.05, 266 | }, 267 | power: { 268 | value: 5, 269 | min: 0, 270 | max: 30, 271 | step: 0.05, 272 | }, 273 | offset: { 274 | value: 0.3, 275 | min: -1, 276 | max: 1, 277 | step: 0.05, 278 | }, 279 | reverse: false, 280 | }), 281 | }; 282 | break; 283 | case "/shapes": 284 | controls = { 285 | Curve: folder({ 286 | positionA: { 287 | value: { x: 0.2, y: 0.2 }, 288 | step: 0.01, 289 | }, 290 | positionB: { 291 | value: { x: 0.2, y: 0.8 }, 292 | step: 0.01, 293 | }, 294 | positionC: { 295 | value: { x: 0.8, y: 0.2 }, 296 | step: 0.01, 297 | }, 298 | positionD: { 299 | value: { x: 0.8, y: 0.8 }, 300 | step: 0.01, 301 | }, 302 | }), 303 | Text: folder({ 304 | value: "This text looks cool in the Texture Tinker Tool", 305 | scale: { 306 | value: 0.5, 307 | min: 0.1, 308 | max: 1.5, 309 | step: 0.01, 310 | }, 311 | alignment: false, 312 | bold: false, 313 | italic: false, 314 | }), 315 | "Rounded Box": folder({ 316 | radius: { 317 | value: 0.1, 318 | min: 0, 319 | max: 0.5, 320 | step: 0.02, 321 | }, 322 | }), 323 | }; 324 | break; 325 | case "/seamless": 326 | controls = { 327 | Seamless: folder({ 328 | url: { 329 | options: { 330 | castle: "castle.jpg", 331 | bricks: "bricks.jpg", 332 | "wooden floor": 333 | "https://plus.unsplash.com/premium_photo-1670159661171-4efef5308566?q=80&w=2972&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", 334 | "egyptian wall": 335 | "https://images.unsplash.com/photo-1622366681698-af1f8305a17f?q=80&w=3174&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", 336 | }, 337 | }, 338 | offsetX: { 339 | value: 0.3, 340 | min: 0, 341 | max: 1, 342 | step: 0.01, 343 | }, 344 | offsetY: { 345 | value: 0.3, 346 | min: 0, 347 | max: 1, 348 | step: 0.01, 349 | }, 350 | sizeX: { 351 | value: 0.2, 352 | min: 0, 353 | max: 0.5, 354 | step: 0.01, 355 | }, 356 | sizeY: { 357 | value: 0.2, 358 | min: 0, 359 | max: 0.5, 360 | step: 0.01, 361 | }, 362 | "offset both": false, 363 | flipX: false, 364 | flipY: false, 365 | alphaOffset: { 366 | value: 0, 367 | min: 0, 368 | max: 1, 369 | step: 0.01, 370 | }, 371 | alphaReverse: false, 372 | }), 373 | }; 374 | break; 375 | case "/renders": 376 | controls = { 377 | Noise: folder({ 378 | "random seed": { 379 | value: 0, 380 | min: 0, 381 | max: 1000000, 382 | step: 1, 383 | }, 384 | "perlin seed": { 385 | value: 0, 386 | min: 0, 387 | max: 1000000, 388 | step: 1, 389 | }, 390 | "perlin detail": { 391 | value: 2, 392 | min: 1, 393 | max: 4, 394 | step: 0.1, 395 | }, 396 | }), 397 | }; 398 | break; 399 | case "/presets/bricks": 400 | controls = { 401 | Noise: folder({ 402 | width: { 403 | value: 0.2, 404 | min: 0.04, 405 | max: 1, 406 | step: 0.02, 407 | }, 408 | height: { 409 | value: 0.2, 410 | min: 0.04, 411 | max: 1, 412 | step: 0.02, 413 | }, 414 | thickness: { 415 | value: 0.02, 416 | min: 0, 417 | max: 0.1, 418 | step: 0.005, 419 | }, 420 | radius: { 421 | value: 0.02, 422 | min: 0, 423 | max: 0.1, 424 | step: 0.005, 425 | }, 426 | offsetX: { 427 | value: 0, 428 | min: 0, 429 | max: 1, 430 | step: 0.02, 431 | }, 432 | "random seed": { 433 | value: 0, 434 | min: 0, 435 | max: 1000000, 436 | step: 1, 437 | }, 438 | randomizeX: { 439 | value: 0, 440 | min: 0, 441 | max: 1, 442 | step: 0.02, 443 | }, 444 | randomizeY: { 445 | value: 0, 446 | min: 0, 447 | max: 1, 448 | step: 0.02, 449 | }, 450 | randomizeS: { 451 | value: 0, 452 | min: 0, 453 | max: 0.2, 454 | step: 0.01, 455 | }, 456 | }), 457 | }; 458 | break; 459 | default: 460 | break; 461 | } 462 | return controls; 463 | }; 464 | 465 | export default getControls; 466 | -------------------------------------------------------------------------------- /examples/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #000000; 3 | margin: 0; 4 | overflow: hidden; 5 | } 6 | -------------------------------------------------------------------------------- /examples/src/index.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App"; 4 | import React from "react"; 5 | import { Router } from "wouter"; 6 | 7 | const root = createRoot(document.getElementById("root")! as HTMLElement); 8 | 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /examples/src/meshes/PrettyBox.tsx: -------------------------------------------------------------------------------- 1 | import { extend } from "@react-three/fiber"; 2 | import { DoubleSide } from "three"; 3 | import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js"; 4 | import PrettyMeshProps from "../types/PrettyMesh"; 5 | 6 | extend({ RoundedBoxGeometry }); 7 | 8 | const PrettyBox = ({ children, ...props }: PrettyMeshProps) => ( 9 | 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | 17 | export default PrettyBox; 18 | -------------------------------------------------------------------------------- /examples/src/meshes/PrettyCylinder.tsx: -------------------------------------------------------------------------------- 1 | import { DoubleSide } from "three"; 2 | import PrettyMeshProps from "../types/PrettyMesh"; 3 | 4 | const PrettyCylinder = ({ children, ...props }: PrettyMeshProps) => { 5 | return ( 6 | 7 | 8 | 9 | {children} 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default PrettyCylinder; 16 | -------------------------------------------------------------------------------- /examples/src/meshes/PrettyPlane.tsx: -------------------------------------------------------------------------------- 1 | import { DoubleSide } from "three"; 2 | import PrettyMeshProps from "../types/PrettyMesh"; 3 | 4 | const PrettyPlane = ({ children, ...props }: PrettyMeshProps) => ( 5 | 6 | 7 | 8 | {children} 9 | 10 | 11 | ); 12 | 13 | export default PrettyPlane; 14 | -------------------------------------------------------------------------------- /examples/src/meshes/PrettySphere.tsx: -------------------------------------------------------------------------------- 1 | import { extend } from "@react-three/fiber"; 2 | import { DoubleSide } from "three"; 3 | import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js"; 4 | import PrettyMeshProps from "../types/PrettyMesh"; 5 | 6 | extend({ RoundedBoxGeometry }); 7 | 8 | const PrettySphere = ({ children, ...props }: PrettyMeshProps) => { 9 | return ( 10 | 11 | 12 | 13 | {children} 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default PrettySphere; 20 | -------------------------------------------------------------------------------- /examples/src/types/Demo.ts: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import PrettyMeshProps from "./PrettyMesh"; 3 | 4 | export default interface DemoProps extends Record { 5 | globalProps: GlobalControlProps; 6 | } 7 | 8 | export type GlobalControlProps = { 9 | mesh: FC; 10 | dimensions: number; 11 | canvas: boolean; 12 | }; 13 | -------------------------------------------------------------------------------- /examples/src/types/PrettyMesh.ts: -------------------------------------------------------------------------------- 1 | import { BoxGeometryProps, MeshProps } from "@react-three/fiber"; 2 | import { ReactNode } from "react"; 3 | 4 | declare global { 5 | namespace JSX { 6 | interface IntrinsicElements { 7 | roundedBoxGeometry: BoxGeometryProps; 8 | } 9 | } 10 | } 11 | 12 | export default interface PrettyMeshProps extends MeshProps, Record { 13 | children?: ReactNode; 14 | } 15 | -------------------------------------------------------------------------------- /examples/src/ui/Navigation.css: -------------------------------------------------------------------------------- 1 | nav { 2 | position: absolute; 3 | z-index: 1; 4 | } 5 | 6 | nav a { 7 | color: #e0e0e0; 8 | font-size: 18px; 9 | font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif; 10 | text-shadow: 1px 1px #000000; 11 | display: inline-block; 12 | height: 16px; 13 | padding: 2px 8px 8px; 14 | margin: 8px 0px 0; 15 | text-decoration: none; 16 | } 17 | 18 | nav a.selected { 19 | color: #ffffff; 20 | } 21 | 22 | nav a.more-info { 23 | position: relative; 24 | color: #ffffff; 25 | font-size: 14px; 26 | background-color: #0044ff44; 27 | border-radius: 16px; 28 | text-shadow: none; 29 | margin: 8px 8px 0; 30 | top: -1px; 31 | } 32 | 33 | nav a:hover { 34 | text-decoration: underline; 35 | } 36 | 37 | nav a.more-info:hover { 38 | text-decoration: none; 39 | background-color: #0044ff88; 40 | } 41 | 42 | .submenu { 43 | position: absolute; 44 | top: 32px; 45 | left: 104px; 46 | z-index: 1; 47 | } 48 | -------------------------------------------------------------------------------- /examples/src/ui/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import "./Navigation.css"; 2 | import Effects from "../demos/Effects"; 3 | import Gradient from "../demos/Gradient"; 4 | import Blending from "../demos/Blending"; 5 | import Transformation from "../demos/Transformation"; 6 | import Filters from "../demos/Filters"; 7 | import getControls from "../helpers/getControls"; 8 | import { folder, useControls } from "leva"; 9 | import PrettyBox from "../meshes/PrettyBox"; 10 | import PrettySphere from "../meshes/PrettySphere"; 11 | import PrettyCylinder from "../meshes/PrettyCylinder"; 12 | import Images from "../demos/Images"; 13 | import { Schema } from "leva/dist/declarations/src/types"; 14 | import Alpha from "../demos/Alpha"; 15 | import { Link, Redirect, Route, useLocation } from "wouter"; 16 | import Shapes from "../demos/Shapes"; 17 | import { GlobalControlProps } from "../types/Demo"; 18 | import { textureDefaults } from "react-three-texture"; 19 | import Misc from "../demos/Misc"; 20 | import Seamless from "../demos/Seamless"; 21 | import Renders from "../demos/Renders"; 22 | import Bricks from "../demos/Bricks"; 23 | import PrettyPlane from "../meshes/PrettyPlane"; 24 | import { Fragment } from "react"; 25 | 26 | const menuItems = [ 27 | { Component: Images, name: "Images" }, 28 | { Component: Transformation, name: "Transformation" }, 29 | { Component: Gradient, name: "Gradient" }, 30 | { Component: Blending, name: "Blending" }, 31 | { Component: Filters, name: "Filters" }, 32 | { Component: Effects, name: "Effects" }, 33 | { Component: Alpha, name: "Alpha" }, 34 | { Component: Shapes, name: "Shapes" }, 35 | { Component: Seamless, name: "Seamless" }, 36 | { Component: Renders, name: "Renders" }, 37 | { 38 | name: "Presets", 39 | submenu: [{ Component: Bricks, name: "Bricks" }], 40 | }, 41 | { Component: Misc, name: "Misc" }, 42 | ]; 43 | 44 | export const Navigation = () => { 45 | const [location] = useLocation(); 46 | 47 | return ( 48 | 92 | ); 93 | }; 94 | 95 | export const Pages = () => { 96 | const globalControl: Schema = { 97 | "Global Settings": folder({ 98 | mesh: { options: { Box: PrettyBox, Sphere: PrettySphere, Cylinder: PrettyCylinder, Plane: PrettyPlane } }, 99 | dimensions: { options: [1024, 512, 256, 128, 64, 32] }, 100 | canvas: { value: false, onChange: (v) => document.querySelector("#textureset__preview")?.classList.toggle("show", v) }, 101 | }), 102 | }; 103 | const globalControlProps = useControls(globalControl) as unknown as GlobalControlProps; 104 | const dimensions = globalControlProps.dimensions as number; 105 | const [location] = useLocation(); 106 | const { ...controlProps } = getControls(location); 107 | textureDefaults({ dimensions }); 108 | 109 | return ( 110 | <> 111 | 112 | 113 | 114 | 115 | {menuItems.map(({ Component: Page, name, submenu }) => ( 116 | 117 | {Page && } 118 | {submenu?.map(({ Component: SubPage, name: subName }) => ( 119 | 120 | 121 | 122 | ))} 123 | 124 | ))} 125 | 126 | 127 | ); 128 | }; 129 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "jsx": "react-jsx", 6 | "strict": true, 7 | "allowSyntheticDefaultImports": true, 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "node", 11 | "strictNullChecks": true, 12 | "noImplicitAny": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-three-texture", 3 | "author": "Jorg Nieberg", 4 | "description": "Live texture editing tool for ThreeJS and React Three Fiber", 5 | "license": "MIT", 6 | "version": "1.1.18", 7 | "private": false, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/jnieberg/react-three-texture.git" 11 | }, 12 | "keywords": [ 13 | "texture", 14 | "layers", 15 | "material", 16 | "image", 17 | "canvas", 18 | "threejs", 19 | "react", 20 | "three", 21 | "fiber", 22 | "r3f", 23 | "tool", 24 | "blending", 25 | "effect", 26 | "gradient" 27 | ], 28 | "devDependencies": { 29 | "@react-three/fiber": "^8.6.2", 30 | "@rollup/plugin-commonjs": "^22.0.2", 31 | "@rollup/plugin-node-resolve": "^13.3.0", 32 | "@rollup/plugin-typescript": "^8.4.0", 33 | "@types/color-string": "^1.5.5", 34 | "@types/react": "^18.2.37", 35 | "@types/three": "^0.158.2", 36 | "copyfiles": "^2.4.1", 37 | "json": "^11.0.0", 38 | "npm-run-all": "^4.1.5", 39 | "react": "^18.2.0", 40 | "react-dom": "^18.2.0", 41 | "rollup": "^2.79.0", 42 | "rollup-plugin-dts": "^4.2.2", 43 | "rollup-plugin-peer-deps-external": "^2.2.4", 44 | "rollup-plugin-postcss": "^4.0.2", 45 | "rollup-plugin-terser": "^7.0.2", 46 | "three": "^0.158.0", 47 | "tslib": "^2.4.0", 48 | "typescript": "^4.8.2" 49 | }, 50 | "peerDependencies": { 51 | "@react-three/fiber": ">=8.0", 52 | "react": ">=18.0", 53 | "react-dom": ">=18.0", 54 | "three": ">=0.137" 55 | }, 56 | "scripts": { 57 | "build": "npm-run-all close build:clean build:copy build:rollup", 58 | "build:clean": "rm -rf dist", 59 | "build:rollup": "rollup -c", 60 | "build:copy": "copyfiles package.json README.md LICENSE dist && json -I -f dist/package.json -e \"this.private=false; this.devDependencies=undefined; this.optionalDependencies=undefined; this.scripts=undefined; this.prettier=undefined; this['lint-staged']=undefined;\"", 61 | "open": "npm-run-all open:marker open:clean open:copy open:replace", 62 | "open:marker": "touch !!EDIT_DO_NOT_PUSH", 63 | "open:clean": "rm -rf ./examples/src/temp", 64 | "open:copy": "cp -R ./src ./examples/src/temp", 65 | "open:replace": "find ./examples/src/demos -type f -name '*.tsx' -exec sed -i '' 's/\"react-three-texture\"/\"\\.\\.\\/temp\"/g' {} \\;", 66 | "save": "npm-run-all save:clean save:copy", 67 | "save:clean": "rm -rf ./src", 68 | "save:copy": "cp -R ./examples/src/temp ./src", 69 | "close": "npm-run-all save close:replace close:marker", 70 | "close:marker": "rm -rf !!EDIT_DO_NOT_PUSH", 71 | "close:replace": "find ./examples/src/demos -type f -name '*.tsx' -exec sed -i '' 's/\".*\\/temp\"/\"react-three-texture\"/g' {} \\;" 72 | }, 73 | "main": "./dist/cjs/index.js", 74 | "module": "./dist/esm/index.js", 75 | "files": [ 76 | "dist" 77 | ], 78 | "types": "./dist/index.d.ts", 79 | "bugs": { 80 | "url": "https://github.com/jnieberg/react-three-texture/issues" 81 | }, 82 | "homepage": "https://github.com/jnieberg/react-three-texture#readme", 83 | "dependencies": { 84 | "color-string": "^1.9.1" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /public/readme-alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-alpha.png -------------------------------------------------------------------------------- /public/readme-blending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-blending.png -------------------------------------------------------------------------------- /public/readme-bloom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-bloom.png -------------------------------------------------------------------------------- /public/readme-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-color.png -------------------------------------------------------------------------------- /public/readme-dimensions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-dimensions.png -------------------------------------------------------------------------------- /public/readme-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-example.png -------------------------------------------------------------------------------- /public/readme-fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-fill.png -------------------------------------------------------------------------------- /public/readme-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-filter.png -------------------------------------------------------------------------------- /public/readme-gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-gradient.png -------------------------------------------------------------------------------- /public/readme-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-image.png -------------------------------------------------------------------------------- /public/readme-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-layer.png -------------------------------------------------------------------------------- /public/readme-nearest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-nearest.png -------------------------------------------------------------------------------- /public/readme-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-outline.png -------------------------------------------------------------------------------- /public/readme-repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-repeat.png -------------------------------------------------------------------------------- /public/readme-seamless.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-seamless.png -------------------------------------------------------------------------------- /public/readme-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-shadow.png -------------------------------------------------------------------------------- /public/readme-shapes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-shapes.png -------------------------------------------------------------------------------- /public/readme-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-src.png -------------------------------------------------------------------------------- /public/readme-transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnieberg/react-three-texture/c720485cd42d0c9fefffefca132655fa462865ac/public/readme-transform.png -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import resolve from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import typescript from "@rollup/plugin-typescript"; 5 | import dts from "rollup-plugin-dts"; 6 | 7 | const packageJson = require("./package.json"); 8 | const external = ["react", "react-dom", "@react-three/fiber", "three"]; 9 | 10 | export default [ 11 | { 12 | input: "src/index.ts", 13 | output: [ 14 | { 15 | file: packageJson.main, 16 | format: "cjs", 17 | sourcemap: true, 18 | }, 19 | { 20 | file: packageJson.module, 21 | format: "esm", 22 | sourcemap: true, 23 | }, 24 | ], 25 | external, 26 | plugins: [resolve(), commonjs(), typescript({ tsconfig: "./tsconfig.json" })], 27 | }, 28 | { 29 | input: "dist/esm/index.d.ts", 30 | output: [{ file: "dist/index.d.ts", format: "esm" }], 31 | external, 32 | plugins: [dts()], 33 | }, 34 | ]; 35 | -------------------------------------------------------------------------------- /src/components/Layer.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { LayerProps } from "../types"; 3 | 4 | export const Layer: FC = () => null; 5 | -------------------------------------------------------------------------------- /src/components/TextureSet.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SRGBColorSpace } from "three"; 3 | import { TextureSetProps } from "../types/TextureSet"; 4 | import { PrimitiveProps } from "@react-three/fiber"; 5 | import { useTextureSet } from "./hooks/useTextureSet"; 6 | 7 | const TextureSet: React.FC = React.forwardRef(({ map, dimensions = 512, children, ...propsMap }, forwardRef) => { 8 | const ref = React.useRef(); 9 | const texture = useTextureSet(children, dimensions); 10 | 11 | React.useImperativeHandle(forwardRef, () => ref.current); 12 | 13 | return !!texture ? : null; 14 | }); 15 | 16 | export { TextureSet }; 17 | -------------------------------------------------------------------------------- /src/components/hooks/useCanvas.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import { LayerProps } from "../../types"; 3 | import { DEFAULT, textureGlobals } from "../../setup"; 4 | import toUUID from "../../helpers/toUUID"; 5 | import { 6 | effectAlpha, 7 | effectBloom, 8 | effectColor, 9 | effectFill, 10 | effectFlip, 11 | effectGradient, 12 | effectImage, 13 | effectNearest, 14 | effectOutline, 15 | effectRepeat, 16 | effectSeamless, 17 | effectShadow, 18 | effectShape, 19 | effectTransformation, 20 | effectNoise, 21 | } from "../../effects"; 22 | import { useEffect, useState } from "react"; 23 | import flattenChildren from "../../helpers/flattenChildren"; 24 | import storage from "../../storage/storage"; 25 | 26 | export const useCanvas = ( 27 | children: React.ReactNode, 28 | dimensions: number = textureGlobals.dimensions || DEFAULT.dimensions 29 | ): HTMLCanvasElement | null => { 30 | const [texture, setTexture] = useState(null); 31 | const layers = flattenChildren(children); 32 | let uuid = toUUID({ ...textureGlobals, layers: layers.map((layer) => layer?.props), dimensions }); 33 | 34 | const ctx = document.createElement("canvas").getContext("2d"); 35 | 36 | const drawAll = async () => { 37 | const textureStored = storage("TEX", uuid); 38 | 39 | return new Promise((resolve) => { 40 | if (typeof textureStored.get() !== "undefined") { 41 | resolve(textureStored.get()); 42 | } else { 43 | if (ctx) { 44 | ctx.canvas.width = dimensions; 45 | ctx.canvas.height = dimensions; 46 | textureStored.set(ctx); 47 | ctx.canvas.id = uuid; 48 | Promise.all( 49 | layers.map(async (layer) => { 50 | const layerProps: LayerProps = { ...textureGlobals, dimensions, ...layer?.props }; 51 | const layerUuid = toUUID(layerProps); 52 | const layerStored = storage("TEX", layerUuid); 53 | if (typeof layerStored.get() !== "undefined") { 54 | const ctxLayer = layerStored.get(); 55 | return { ctxLayer, canvas: ctxLayer.canvas, props: layerProps }; 56 | } else { 57 | const ctxLayer = document.createElement("canvas").getContext("2d"); 58 | if (ctxLayer) { 59 | ctxLayer.canvas.width = ctx.canvas.width; 60 | ctxLayer.canvas.height = ctx.canvas.height; 61 | await drawLayer(ctxLayer, layerProps); 62 | layerStored.set(ctxLayer); 63 | return { ctxLayer, canvas: ctxLayer.canvas, props: layerProps }; 64 | } 65 | } 66 | return null; 67 | }) 68 | ).then((all: ({ ctxLayer: CanvasRenderingContext2D; canvas: HTMLCanvasElement; props: LayerProps } | null)[]) => { 69 | // Draw each layer 70 | all.forEach((layer) => { 71 | if (layer) { 72 | const cw = layer.props.dimensions || layer.canvas.width; 73 | const ch = layer.props.dimensions || layer.canvas.height; 74 | 75 | if (layer.canvas) { 76 | // Blending 77 | ctx.globalCompositeOperation = layer.props.blend || DEFAULT.blend; 78 | 79 | // Alpha 80 | ctx.globalAlpha = typeof layer.props.alpha === "number" ? layer.props.alpha : 1.0; 81 | 82 | // Nearest neighbour rendering 83 | effectNearest(ctx, layer.props); 84 | 85 | // Apply filters 86 | ctx.filter = layer.props.filter || DEFAULT.filter; 87 | 88 | ctx.drawImage(layer.canvas, 0, 0, cw, ch, 0, 0, layer.canvas.width, layer.canvas.height); 89 | } 90 | } 91 | }); 92 | resolve(ctx); 93 | }); 94 | } 95 | } 96 | }); 97 | }; 98 | 99 | const setEffects = async (ctxLayer: CanvasRenderingContext2D, props: LayerProps) => { 100 | effectNearest(ctxLayer, props); 101 | if (!props.repeat) effectFlip(ctxLayer, props); 102 | await effectTransformation(ctxLayer, props, async (transform) => { 103 | effectFill(ctxLayer, props); 104 | effectNoise(ctxLayer, props); 105 | effectGradient(ctxLayer, props); 106 | effectImage(ctxLayer, props); 107 | await effectShape(ctxLayer, props); 108 | effectRepeat(ctxLayer, props, transform); 109 | }); 110 | effectSeamless(ctxLayer, props); 111 | effectColor(ctxLayer, props); 112 | effectAlpha(ctxLayer, props); 113 | effectShadow(ctxLayer, props); 114 | effectOutline(ctxLayer, props); 115 | effectBloom(ctxLayer, props); 116 | }; 117 | 118 | const setImage = (ctxLayer: CanvasRenderingContext2D, props: LayerProps) => { 119 | return new Promise((resolve) => { 120 | const src = props.src; 121 | let srcString: string; 122 | if (src) { 123 | let img = document.createElement("img"); 124 | const imgSrc = storage("IMG", src).get(); 125 | if (imgSrc) { 126 | srcString = imgSrc.src; 127 | } else { 128 | storage("IMG", src).set(img); 129 | if (src.search(/^(blob:)?https?:\/\//) === 0) { 130 | img.crossOrigin = "Anonymous"; 131 | srcString = src; 132 | } else { 133 | srcString = require(`/src/assets/${src}`); 134 | } 135 | } 136 | img.src = srcString; 137 | img.onerror = (ev) => { 138 | console.error("Image not found:", ev); 139 | }; 140 | img.onload = () => { 141 | setEffects(ctxLayer, props).then(() => resolve()); 142 | }; 143 | } 144 | }); 145 | }; 146 | 147 | const drawLayer = (ctxLayer: CanvasRenderingContext2D, props: LayerProps) => { 148 | return new Promise((resolve) => { 149 | if (props.src) { 150 | setImage(ctxLayer, props).then(() => resolve()); 151 | } else { 152 | setEffects(ctxLayer, props).then(() => resolve()); 153 | } 154 | }); 155 | }; 156 | 157 | useEffect(() => { 158 | drawAll().then((tex) => { 159 | if (tex) setTexture(tex.canvas); 160 | }); 161 | }, [uuid]); 162 | 163 | return texture; 164 | }; 165 | -------------------------------------------------------------------------------- /src/components/hooks/useTextureSet.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-hooks/exhaustive-deps */ 2 | import * as React from "react"; 3 | import { TextureResult } from "../../types"; 4 | import { useCanvas } from "./useCanvas"; 5 | import { CanvasTexture } from "three"; 6 | import { PrimitiveProps } from "@react-three/fiber"; 7 | 8 | export const useTextureSet = (children: React.ReactNode, dimensions: number, isEnvMap: boolean = false): TextureResult => { 9 | const [texture, setTexture] = React.useState(); 10 | const ref = React.useRef(); 11 | const canvas = useCanvas(children, dimensions); 12 | const domPreview = "#textureset__preview"; 13 | const domTexturePreview = `${domPreview} .texture`; 14 | 15 | React.useEffect(() => { 16 | if (canvas) { 17 | setTexture(new CanvasTexture(canvas)); 18 | if (ref.current) ref.current.needsUpdate = true; 19 | 20 | if (document.querySelector(domTexturePreview)) { 21 | document.querySelector(domTexturePreview)?.prepend(canvas); 22 | } 23 | return () => { 24 | document.querySelector(domTexturePreview)?.removeChild(canvas); 25 | canvas.remove(); 26 | }; 27 | } 28 | }, [canvas]); 29 | 30 | return texture as CanvasTexture; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Layer"; 2 | export * from "./TextureSet"; 3 | export * from "./hooks/useTextureSet"; 4 | -------------------------------------------------------------------------------- /src/components/presets/Bricks.tsx: -------------------------------------------------------------------------------- 1 | import Random from "../../helpers/Random"; 2 | import { Layer } from "../Layer"; 3 | import { BricksProps } from "../../types"; 4 | 5 | const Bricks = (props: BricksProps) => { 6 | const { 7 | color = "white", 8 | width = 0.5, 9 | height = 0.5, 10 | thickness = 0.01, 11 | offsetX = 0, 12 | seed = 0, 13 | randomize = [0.0, 0.0, 0.0], 14 | radius = 0.01, 15 | layer, 16 | } = props; 17 | const [shiftX = 0, shiftY = 0, shiftS = 0] = randomize; 18 | const random = new Random(`bricks_${seed}`); 19 | const sx = () => random.float(-shiftX * 0.5, shiftX * 0.5); 20 | const sy = () => random.float(-shiftY * 0.5, shiftY * 0.5); 21 | const ss = () => random.float(-shiftS * 0.5, shiftS * 0.5); 22 | const horizontals = new Array(Math.ceil(1.0 / height)) 23 | .fill(null) 24 | .map((_, y) => (y + (y > 0 && y < Math.ceil(1.0 / height) - 1 ? sy() : 0)) * height); 25 | const verticals = new Array(Math.ceil(1.0 / height)) 26 | .fill(null) 27 | .map((_, y) => new Array(Math.ceil(1.0 / width)).fill(null).map((_, x) => (x + sx()) * width + ((y * offsetX) % 1))); 28 | const verticalsS = verticals.map((vert) => vert.map((v) => ss())); 29 | const bricks: ({ x1: number; y1: number; x2: number; y2: number; s1: number; s2: number; color: string } | null)[] = []; 30 | 31 | horizontals.forEach((h1, i) => 32 | verticals[i].forEach((v1, j) => { 33 | const v2 = verticals[i][(j + 1) % verticals[i].length] + (j < verticals[i].length - 1 ? 0 : 1); 34 | const h2 = horizontals[(i + 1) % horizontals.length] + (i < horizontals.length - 1 ? 0 : 1); 35 | const vs1 = verticalsS[i][j]; 36 | const vs2 = verticalsS[i][(j + 1) % verticalsS[i].length]; 37 | bricks.push( 38 | ...new Array(2).fill(null).map((_, o) => { 39 | let x1 = v1 + thickness * 0.5; 40 | let y1 = h1 + thickness * 0.5; 41 | let x2 = v2 - thickness * 0.5; 42 | let y2 = h2 - thickness * 0.5; 43 | const xmin1 = Math.floor(x1 - Math.abs(vs1)); 44 | const xmin2 = Math.floor(x2 + Math.abs(vs2)); 45 | if (o > 0) { 46 | if (xmin1 === 0 && xmin2 > 0) { 47 | x1 = x1 - xmin2; 48 | x2 = x2 - xmin2; 49 | } else if (xmin1 < 0 && xmin2 === 0) { 50 | x2 = x2 - xmin1; 51 | x1 = x1 - xmin1; 52 | } else { 53 | return null; 54 | } 55 | } else if (o === 0) { 56 | if (xmin1 > 0) { 57 | x2 = x2 - xmin1; 58 | x1 = x1 - xmin1; 59 | } 60 | } 61 | const newColor = Array.isArray(color) ? color : [color]; 62 | const randomColor = new Random(`bricks_color_${seed}_${i}_${j}`); 63 | const c = randomColor.int(0, newColor.length - 1); 64 | return { x1: x1 - vs1, y1, x2: x2 - vs2, y2, s1: x1 + vs1, s2: x2 + vs2, color: newColor[c] }; 65 | }) 66 | ); 67 | }) 68 | ); 69 | 70 | return ( 71 | <> 72 | {bricks.map( 73 | (p, o) => p && 74 | )} 75 | 76 | ); 77 | }; 78 | 79 | export default Bricks; 80 | -------------------------------------------------------------------------------- /src/effects/alpha.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT } from "../setup"; 2 | import { AlphaProps, LayerProps } from "../types"; 3 | 4 | export const effectAlpha = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 5 | if (props.alpha && typeof props.alpha !== "number") { 6 | const { level, power, offset, reverse } = { ...DEFAULT.alpha, ...(props.alpha as AlphaProps) }; 7 | const imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height); 8 | for (let i = 0, n = imageData.data.length; i < n; i += 4) { 9 | let pixelAlpha = (imageData.data[i] + imageData.data[i + 1] + imageData.data[i + 2] + imageData.data[i + 3]) / (255 * 4); 10 | pixelAlpha = (pixelAlpha + offset) ** power; 11 | pixelAlpha = Math.max(0, Math.min(pixelAlpha, 1.0)); 12 | pixelAlpha = reverse ? 1 - pixelAlpha : pixelAlpha; 13 | pixelAlpha *= level; 14 | imageData.data[i + 3] = pixelAlpha * 255; 15 | } 16 | ctx.globalCompositeOperation = "source-over"; 17 | ctx.putImageData(imageData, 0, 0); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/effects/bloom.ts: -------------------------------------------------------------------------------- 1 | import newCanvasHelper from "../helpers/newCanvasHelper"; 2 | import { DEFAULT } from "../setup"; 3 | import { BloomProps, LayerProps } from "../types"; 4 | 5 | export const effectBloom = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 6 | if (props.bloom) { 7 | const { size, strength, softness, detail, darken } = { ...DEFAULT.bloom, ...(props.bloom as BloomProps) }; 8 | newCanvasHelper(ctx, (ctxBloom) => { 9 | ctxBloom.drawImage(ctx.canvas, 0, 0); 10 | 11 | ctxBloom.globalCompositeOperation = darken ? "darken" : "lighten"; 12 | 13 | // Sizes 14 | const dims = props.dimensions || ctx.canvas.width; 15 | const sizeDefault = 32; 16 | const sizeFactor = size / sizeDefault; 17 | let s = sizeDefault; 18 | for (; s >= 0; s--) { 19 | ctxBloom.globalAlpha = ((1.0 - (s / sizeDefault) ** (1.0 - softness)) * strength) / (sizeDefault * Math.sqrt(detail) * 0.1); 20 | 21 | // Rotations 22 | const sizeNormalized = s * sizeFactor * (dims / DEFAULT.dimensions); 23 | let i = 0; 24 | for (; i < detail; i++) { 25 | const angle = Math.PI * 2 * (i / detail) + Math.PI * 0.25; 26 | const direction = [Math.sin(angle), Math.cos(angle)]; 27 | ctxBloom.drawImage(ctx.canvas, direction[0] * sizeNormalized, direction[1] * sizeNormalized, ctx.canvas.width, ctx.canvas.height); 28 | } 29 | } 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/effects/color.ts: -------------------------------------------------------------------------------- 1 | import newCanvasHelper from "../helpers/newCanvasHelper"; 2 | import { DEFAULT } from "../setup"; 3 | import { LayerProps } from "../types"; 4 | 5 | export const effectColor = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 6 | const isShape = props.circle || props.curve || props.line || props.rect || props.text; 7 | if (props.color && !isShape) { 8 | newCanvasHelper(ctx, (ctxColor) => { 9 | ctxColor.drawImage(ctx.canvas, 0, 0); 10 | ctxColor.globalCompositeOperation = "source-in"; 11 | ctxColor.fillStyle = !props.color || typeof props.color === "boolean" ? DEFAULT.color : props.color; 12 | ctxColor.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 13 | // TO DO - find a way to make this generally work for both SVG and other image types 14 | if (props.src?.search(/\.svg$/) === -1) { 15 | ctxColor.filter = "grayscale(100%)"; 16 | ctxColor.globalCompositeOperation = "hard-light"; 17 | ctxColor.drawImage(ctx.canvas, 0, 0); 18 | } 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/effects/fill.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT } from "../setup"; 2 | import { LayerProps } from "../types"; 3 | 4 | export const effectFill = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 5 | if (props.fill) { 6 | ctx.fillStyle = typeof props.fill === "boolean" ? DEFAULT.fill : props.fill; 7 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/effects/flip.ts: -------------------------------------------------------------------------------- 1 | import { LayerProps } from "../types"; 2 | 3 | export const effectFlip = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 4 | if (props.flipX) { 5 | ctx.scale(-1, 1); 6 | ctx.translate(-ctx.canvas.width, 0); 7 | } 8 | if (props.flipY) { 9 | ctx.scale(1, -1); 10 | ctx.translate(0, -ctx.canvas.height); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/effects/gradient.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT } from "../setup"; 2 | import { GradientLinearProps, GradientRadialProps, LayerProps } from "../types"; 3 | 4 | export const effectGradient = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 5 | if (props.gradient) { 6 | const gradientProp = props.gradient as GradientLinearProps | GradientRadialProps; 7 | const { type, from, to, stops } = { ...DEFAULT.gradient[gradientProp.type || DEFAULT.gradient.type], ...gradientProp }; 8 | const dims = props.dimensions || ctx.canvas.width; 9 | let args: number[] = [...from, ...to]; 10 | args = args.map((a, i) => args[i] * dims); 11 | 12 | const canvasGradient = 13 | type === "radial" 14 | ? ctx.createRadialGradient(args[0], args[1], args[2], args[3], args[4], args[5]) // weird TS thing 15 | : ctx.createLinearGradient(args[0], args[1], args[2], args[3]); 16 | stops.forEach((stop) => canvasGradient.addColorStop(stop[0], stop[1])); 17 | ctx.fillStyle = canvasGradient; 18 | ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/effects/image.ts: -------------------------------------------------------------------------------- 1 | import storage from "../storage/storage"; 2 | import { LayerProps } from "../types"; 3 | 4 | export const effectImage = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 5 | const src = props.src; 6 | const target = storage("IMG", src).get(); 7 | if (target) ctx.drawImage(target, 0, 0, ctx.canvas.width, ctx.canvas.height); 8 | }; 9 | -------------------------------------------------------------------------------- /src/effects/index.ts: -------------------------------------------------------------------------------- 1 | export { effectImage } from "./image"; 2 | export { effectRepeat } from "./repeat"; 3 | export { effectAlpha } from "./alpha"; 4 | export { effectBloom } from "./bloom"; 5 | export { effectColor } from "./color"; 6 | export { effectFill } from "./fill"; 7 | export { effectGradient } from "./gradient"; 8 | export { effectOutline } from "./outline"; 9 | export { effectShadow } from "./shadow"; 10 | export { effectTransformation } from "./transformation"; 11 | export { effectShape } from "./shape"; 12 | export { effectNearest } from "./nearest"; 13 | export { effectFlip } from "./flip"; 14 | export { effectSeamless } from "./seamless"; 15 | export { effectNoise } from "./noise"; 16 | -------------------------------------------------------------------------------- /src/effects/nearest.ts: -------------------------------------------------------------------------------- 1 | import { LayerProps } from "../types"; 2 | 3 | export const effectNearest = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 4 | ctx.imageSmoothingQuality = "low"; 5 | ctx.imageSmoothingEnabled = !props.nearest; 6 | }; 7 | -------------------------------------------------------------------------------- /src/effects/noise.ts: -------------------------------------------------------------------------------- 1 | import Random from "../helpers/Random"; 2 | import { mixColors } from "../helpers/mixColors"; 3 | import newCanvasHelper from "../helpers/newCanvasHelper"; 4 | import { rgbToHex } from "../helpers/rgbToHex"; 5 | import { LayerProps } from "../types"; 6 | import * as ColorString from "color-string"; 7 | 8 | export const effectNoise = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 9 | const noise = props.noise; 10 | if (noise) { 11 | const random = new Random(`noise_${noise.seed}`); 12 | 13 | const getRand = () => { 14 | let rand = 0; 15 | if (noise?.seed) { 16 | rand = random.float(); 17 | } else { 18 | rand = Math.random(); 19 | } 20 | return rand; 21 | }; 22 | 23 | newCanvasHelper( 24 | ctx, 25 | (ctxNoise) => { 26 | const w = 1024; //ctxNoise.canvas.width; 27 | const h = 1024; //ctxNoise.canvas.height; 28 | ctxNoise.canvas.width = w; 29 | ctxNoise.canvas.height = h; 30 | 31 | const randomNoise = (): CanvasRenderingContext2D => { 32 | const fromColor = ColorString.get.rgb(noise.from || "white"); 33 | const toColor = ColorString.get.rgb(noise.to || "black"); 34 | const iData = ctxNoise.createImageData(w, h); 35 | const buffer32 = new Uint32Array(iData.data.buffer); 36 | const len = buffer32.length; 37 | let i = 0; 38 | for (; i < len; i++) { 39 | const mixColor = mixColors(fromColor, toColor, getRand()); 40 | buffer32[i] = rgbToHex(...mixColor); 41 | } 42 | ctxNoise.putImageData(iData, 0, 0); 43 | return ctxNoise; 44 | }; 45 | 46 | const perlinNoise = (): CanvasRenderingContext2D => { 47 | newCanvasHelper(ctxNoise, (ctxNoise2) => { 48 | ctxNoise = randomNoise(); 49 | let alpha = 1; 50 | for (var size = 4; size <= w; size *= 2) { 51 | var x = Math.floor(getRand() * (w - size)); 52 | var y = Math.floor(getRand() * (h - size)); 53 | ctxNoise2.globalAlpha = alpha; 54 | alpha /= noise?.detail || 2; 55 | ctxNoise2.drawImage(ctxNoise.canvas, x, y, size, size, 0, 0, w, h); 56 | } 57 | }); 58 | return ctxNoise; 59 | }; 60 | 61 | switch (noise.type) { 62 | case "random": 63 | ctxNoise = randomNoise(); 64 | break; 65 | case "perlin": 66 | ctxNoise = perlinNoise(); 67 | break; 68 | default: 69 | break; 70 | } 71 | ctx.drawImage(ctxNoise.canvas, 0, 0, w, h, 0, 0, ctx.canvas.width, ctx.canvas.height); 72 | }, 73 | false 74 | ); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/effects/outline.ts: -------------------------------------------------------------------------------- 1 | import newCanvasHelper from "../helpers/newCanvasHelper"; 2 | import { DEFAULT } from "../setup"; 3 | import { LayerProps, OutlineProps } from "../types"; 4 | 5 | export const effectOutline = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 6 | if (props.outline) { 7 | const { color, size, detail } = { ...DEFAULT.outline, ...(props.outline as OutlineProps) }; 8 | newCanvasHelper(ctx, (ctxOutline) => { 9 | ctxOutline.fillStyle = color; 10 | ctxOutline.fillRect(0, 0, ctxOutline.canvas.width, ctxOutline.canvas.height); 11 | 12 | // The actual outlining 13 | newCanvasHelper(ctxOutline, (ctxOutlineIn) => { 14 | const dims = props.dimensions || ctx.canvas.width; 15 | const sizeNorm = size * (dims / DEFAULT.dimensions); 16 | const detailNorm = detail; 17 | let i = 0; 18 | ctxOutline.globalCompositeOperation = "destination-in"; 19 | for (; i < detailNorm; i++) { 20 | const angle = Math.PI * 2 * (i / detailNorm) + Math.PI * 0.25; 21 | const direction = [Math.sin(angle), Math.cos(angle)]; 22 | ctxOutlineIn.drawImage(ctx.canvas, direction[0] * sizeNorm, direction[1] * sizeNorm, ctxOutlineIn.canvas.width, ctxOutlineIn.canvas.height); 23 | } 24 | }); 25 | 26 | ctxOutline.globalCompositeOperation = "source-over"; 27 | ctxOutline.drawImage(ctx.canvas, 0, 0, ctxOutline.canvas.width, ctxOutline.canvas.height); 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/effects/repeat.ts: -------------------------------------------------------------------------------- 1 | import newCanvasHelper from "../helpers/newCanvasHelper"; 2 | import { LayerProps, TransformReturn } from "../types"; 3 | import { effectFlip } from "."; 4 | 5 | export const effectRepeat = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps, transform?: TransformReturn) => { 6 | if (props.repeat) { 7 | const texSize = [ctx.canvas.width, ctx.canvas.height]; 8 | 9 | newCanvasHelper( 10 | ctx, 11 | (ctxRepeat) => { 12 | if (transform) { 13 | const { position, scale, rotation } = transform; 14 | ctxRepeat.canvas.width = scale[0]; 15 | ctxRepeat.canvas.height = scale[1]; 16 | ctxRepeat.drawImage(ctx.canvas, 0, 0, texSize[0], texSize[1], 0, 0, scale[0], scale[1]); 17 | ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); 18 | 19 | ctx.translate(texSize[0] * 0.5, texSize[1] * 0.5); 20 | ctx.rotate(rotation); 21 | ctx.translate(-texSize[0] * 0.5, -texSize[1] * 0.5); 22 | effectFlip(ctx, props); 23 | 24 | const offset = position.map((p, i) => (((p % scale[i]) + scale[i]) % scale[i]) - scale[i]); 25 | const repeat = scale.map((s, i) => Math.ceil(texSize[i] / s)); 26 | for (let y = -Math.ceil(repeat[1] * 0.2); y <= Math.ceil(repeat[1] * 1.2); y += 1) { 27 | for (let x = -Math.ceil(repeat[0] * 0.2); x <= Math.ceil(repeat[0] * 1.2); x += 1) { 28 | ctx.drawImage(ctxRepeat.canvas, offset[0] + x * scale[0], offset[1] + y * scale[1], scale[0], scale[1]); 29 | } 30 | } 31 | } 32 | }, 33 | false 34 | ); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/effects/seamless.ts: -------------------------------------------------------------------------------- 1 | import newCanvasHelper from "../helpers/newCanvasHelper"; 2 | import { DEFAULT } from "../setup"; 3 | import { LayerProps, SeamlessProps } from "../types"; 4 | import { effectAlpha, effectFlip, effectGradient } from "."; 5 | 6 | export const effectSeamless = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 7 | if (props.seamless) { 8 | const { offset, size, both, alphaOffset, alphaReverse, flipX, flipY } = { ...DEFAULT.seamless, ...(props.seamless as SeamlessProps) }; 9 | const xyArr = both 10 | ? [[1, 1, 1, 0]] 11 | : [ 12 | [1, 0, 1, 0], 13 | [0, 1, 0, 1], 14 | ]; 15 | xyArr.forEach(([x, y, u, v], i) => { 16 | newCanvasHelper(ctx, (ctxSeamless) => { 17 | if (size[i] > 0) { 18 | ctxSeamless.globalCompositeOperation = "source-over"; 19 | effectFlip(ctxSeamless, { flipX: flipX, flipY: flipY }); 20 | ctxSeamless.translate(-ctx.canvas.width * offset[0] * x, -ctx.canvas.height * offset[1] * y); 21 | ctxSeamless.drawImage(ctx.canvas, 0, 0); 22 | ctxSeamless.drawImage(ctx.canvas, ctx.canvas.width * x, ctx.canvas.height * y); 23 | if (both) { 24 | ctxSeamless.drawImage(ctx.canvas, ctx.canvas.width * x, 0); 25 | ctxSeamless.drawImage(ctx.canvas, 0, ctx.canvas.height * y); 26 | } 27 | ctxSeamless.resetTransform(); 28 | if (alphaOffset > 0) { 29 | ctxSeamless.globalCompositeOperation = "source-in"; 30 | effectAlpha(ctxSeamless, { alpha: { power: 100, offset: alphaOffset, reverse: alphaReverse } }); 31 | } 32 | ctxSeamless.globalCompositeOperation = "destination-in"; 33 | newCanvasHelper(ctxSeamless, (ctxGradient) => { 34 | effectGradient(ctxGradient, { 35 | gradient: { 36 | from: [0, 0], 37 | to: [u, v], 38 | stops: [ 39 | [0, "#ffffffff"], 40 | [size[i], "#ffffff00"], 41 | [1 - size[i], "#ffffff00"], 42 | [1, "#ffffffff"], 43 | ], 44 | }, 45 | }); 46 | if (both) { 47 | effectGradient(ctxGradient, { 48 | gradient: { 49 | from: [0, 0], 50 | to: [v, u], 51 | stops: [ 52 | [0, "#ffffffff"], 53 | [size[1], "#ffffff00"], 54 | [1 - size[1], "#ffffff00"], 55 | [1, "#ffffffff"], 56 | ], 57 | }, 58 | }); 59 | } 60 | }); 61 | } 62 | }); 63 | }); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/effects/shadow.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT } from "../setup"; 2 | import { LayerProps, ShadowProps } from "../types"; 3 | 4 | export const effectShadow = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 5 | if (props.shadow) { 6 | const { color, blur, offset } = { ...DEFAULT.shadow, ...(props.shadow as ShadowProps) }; 7 | const dims = props.dimensions || ctx.canvas.width; 8 | ctx.shadowBlur = blur * (dims / DEFAULT.dimensions); 9 | ctx.shadowColor = color; 10 | ctx.shadowOffsetX = offset[0]; 11 | ctx.shadowOffsetY = offset[1]; 12 | ctx.globalCompositeOperation = "source-over"; 13 | ctx.drawImage(ctx.canvas, 0, 0, ctx.canvas.width, ctx.canvas.height); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/effects/shape.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT } from "../setup"; 2 | import { LayerProps } from "../types"; 3 | import { shapeLine, shapeCircle, shapeRect, shapeCurve, shapeText, shapePoly } from "./shapes"; 4 | 5 | export const effectShape = async (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 6 | const dims = props.dimensions || ctx.canvas.width; 7 | const color = !props.color || typeof props.color === "boolean" ? DEFAULT.color : props.color; 8 | const scale = props.scale?.[0] || 1; 9 | const thick: number = props.shapeThickness || DEFAULT.shapeThickness; 10 | if (!!thick) { 11 | ctx.strokeStyle = color; 12 | ctx.lineWidth = (thick * (dims / DEFAULT.dimensions)) / scale; 13 | } else { 14 | ctx.fillStyle = color; 15 | } 16 | ctx.lineCap = props.shapeRounded ? "round" : "butt"; 17 | ctx.lineJoin = props.shapeRounded ? "round" : "miter"; 18 | 19 | shapeLine(ctx, props); 20 | shapeCircle(ctx, props); 21 | shapeRect(ctx, props); 22 | shapePoly(ctx, props); 23 | shapeCurve(ctx, props); 24 | await shapeText(ctx, props); 25 | 26 | !!props.shapeThickness ? ctx.stroke() : ctx.fill(); 27 | }; 28 | -------------------------------------------------------------------------------- /src/effects/shapes/circle.ts: -------------------------------------------------------------------------------- 1 | import { LayerProps } from "../../types"; 2 | 3 | export const shapeCircle = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 4 | const circle = props.circle; 5 | if (circle) { 6 | const dims = props.dimensions || ctx.canvas.width; 7 | const circleArgs: [number, number, number, number, number, number, number, boolean?] = [ 8 | circle[0] * dims, 9 | circle[1] * dims, 10 | circle[2] * dims, 11 | (circle[3] || circle[2]) * dims, 12 | circle[4] || 0, 13 | circle[5] || 0, 14 | circle[6] || Math.PI * 2, 15 | circle[7], 16 | ]; 17 | ctx.ellipse(...circleArgs); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/effects/shapes/curve.ts: -------------------------------------------------------------------------------- 1 | import { LayerProps } from "../../types"; 2 | 3 | export const shapeCurve = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 4 | const curve = props.curve; 5 | if (curve) { 6 | const dims = props.dimensions || ctx.canvas.width; 7 | const curveArgs: [number, number, number, number, number, number, number?, number?] = [ 8 | curve[0] * dims, 9 | curve[1] * dims, 10 | curve[2] * dims, 11 | curve[3] * dims, 12 | curve[4] * dims, 13 | curve[5] * dims, 14 | curve[6] ? curve[6] * dims : undefined, 15 | curve[7] ? curve[7] * dims : undefined, 16 | ]; 17 | ctx.moveTo(curveArgs[0], curveArgs[1]); 18 | if (curveArgs[6] && curveArgs[7]) { 19 | ctx.bezierCurveTo(curveArgs[2], curveArgs[3], curveArgs[4], curveArgs[5], curveArgs[6], curveArgs[7]); 20 | } else { 21 | ctx.quadraticCurveTo(curveArgs[2], curveArgs[3], curveArgs[4], curveArgs[5]); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/effects/shapes/index.ts: -------------------------------------------------------------------------------- 1 | export { shapeCircle } from "./circle"; 2 | export { shapeCurve } from "./curve"; 3 | export { shapeLine } from "./line"; 4 | export { shapeRect } from "./rect"; 5 | export { shapePoly } from "./poly"; 6 | export { shapeText } from "./text"; 7 | -------------------------------------------------------------------------------- /src/effects/shapes/line.ts: -------------------------------------------------------------------------------- 1 | import { LayerProps } from "../../types"; 2 | 3 | export const shapeLine = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 4 | const line = props.line; 5 | if (line) { 6 | const dims = props.dimensions || ctx.canvas.width; 7 | ctx.beginPath(); 8 | for (var i = 0; i < line.length; i += 2) { 9 | const x = line[i]; 10 | const y = line[i + 1]; 11 | if (i === 0) { 12 | ctx.moveTo(x * dims, y * dims); 13 | } else { 14 | ctx.lineTo(x * dims, y * dims); 15 | } 16 | } 17 | if (line[0] === line[line.length - 2] && line[1] === line[line.length - 1]) ctx.closePath(); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/effects/shapes/poly.ts: -------------------------------------------------------------------------------- 1 | import { ctxRoundPoly } from "../../polyfill/ctx"; 2 | import { LayerProps } from "../../types"; 3 | 4 | export const shapePoly = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 5 | const poly = props.poly; 6 | if (poly) { 7 | const dims = props.dimensions || ctx.canvas.width; 8 | const polyArgs = poly 9 | .filter((p, i) => i % 2 === 0 && i < poly.length - 1) 10 | .map((p, j) => ({ 11 | x: p * dims, 12 | y: poly[j * 2 + 1] * dims, 13 | })); 14 | ctxRoundPoly(ctx, polyArgs, poly[poly.length - 1] * dims); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/effects/shapes/rect.ts: -------------------------------------------------------------------------------- 1 | import { ctxRoundRect } from "../../polyfill/ctx"; 2 | import { LayerProps } from "../../types"; 3 | 4 | export const shapeRect = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 5 | const rect = props.rect; 6 | if (rect) { 7 | const dims = props.dimensions || ctx.canvas.width; 8 | const rectArgs: [number, number, number, number, number] = [ 9 | rect[0] * dims, 10 | rect[1] * dims, 11 | rect[2] * dims, 12 | (rect[3] || rect[2]) * dims, 13 | (rect[4] || 0) * dims, 14 | ]; 15 | !!rectArgs[4] ? ctxRoundRect(ctx, ...rectArgs) : ctx.rect(rectArgs[0], rectArgs[1], rectArgs[2], rectArgs[3]); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/effects/shapes/text.ts: -------------------------------------------------------------------------------- 1 | import wrapText from "../../helpers/wrapText"; 2 | import { DEFAULT } from "../../setup"; 3 | import storage from "../../storage/storage"; 4 | import { LayerProps } from "../../types"; 5 | 6 | export const shapeText = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, props: LayerProps) => { 7 | const text = props.text; 8 | if (text) { 9 | return new Promise((resolve) => { 10 | const { font, style, weight, height, value, width, align, base } = { ...DEFAULT.text, ...text }; 11 | const dims = props.dimensions || ctx.canvas.width; 12 | const scale = props.scale?.[0] || 1; 13 | const wrapWidth = (width * dims) / scale; 14 | 15 | const newSize = dims; 16 | const name = font.replace(/(\.[\w]*?$|\W)/g, "").slice(-48) || "Font"; 17 | const drawText = () => { 18 | const fontString = `${style} ${weight} ${newSize}px ${name}`; 19 | ctx.font = fontString; 20 | const textLines = width > 0 ? wrapText(ctx, value, wrapWidth) : [value]; 21 | textLines.forEach((line, index) => { 22 | let offset = index * height * dims; 23 | offset = base === "bottom" ? offset - (textLines.length - 1) * height * dims : offset; 24 | offset = base === "middle" ? offset - (textLines.length - 1) * height * dims * 0.5 : offset; 25 | const posProps = [0, offset] as const; 26 | !!props.shapeThickness ? ctx.strokeText(line, ...posProps) : ctx.fillText(line, ...posProps); 27 | }); 28 | }; 29 | ctx.textAlign = align; 30 | ctx.textBaseline = base; 31 | if (font.indexOf(".") > 0) { 32 | let fontFace = storage("FON", font).get(); 33 | if (!fontFace) { 34 | const srcString = font.search(/^(blob:)?https?:\/\//) === 0 ? font : require(`/src/assets/${font}`); 35 | fontFace = new FontFace(name, `url(${srcString})`); 36 | storage("FON", font).set(fontFace); 37 | } 38 | fontFace.load().then((f) => { 39 | (document as any).fonts.add(f); 40 | drawText(); 41 | resolve(); 42 | }); 43 | } else { 44 | drawText(); 45 | resolve(); 46 | } 47 | }); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/effects/transformation.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT } from "../setup"; 2 | import storage from "../storage/storage"; 3 | import { LayerProps, TransformReturn } from "../types"; 4 | 5 | export const effectTransformation = async ( 6 | ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, 7 | props: LayerProps, 8 | callback: (transform?: TransformReturn) => Promise | void = () => {} 9 | ) => { 10 | const imgSrc = props.src && storage("IMG", props.src).get(); 11 | const target = imgSrc || ctx.canvas; 12 | const tw = target.width; 13 | const th = target.height; 14 | 15 | let scale: [number, number] = [...(props.scale || DEFAULT.scale)]; 16 | let position: [number, number] = [...(props.position || DEFAULT.position)]; 17 | const rotation = props.rotation || DEFAULT.rotation; 18 | const dims = props.dimensions || ctx.canvas.width; 19 | 20 | if (props.fit) { 21 | if (typeof props.fit === "boolean") props.fit = DEFAULT.image; 22 | props.fit.split(" ").forEach((align) => { 23 | if (align === "size-max") { 24 | align = th > tw ? "size-x" : "size-y"; 25 | } else if (align === "size-min") { 26 | align = th < tw ? "size-x" : "size-y"; 27 | } 28 | if (align === "size-x") { 29 | scale = [scale[0], scale[1] * (th / tw)]; 30 | } else if (align === "size-y") { 31 | scale = [scale[0] * (tw / th), scale[1]]; 32 | } else if (align === "size-none") { 33 | scale = [scale[0] * (tw / ctx.canvas.width), scale[1] * (th / ctx.canvas.height)]; 34 | } 35 | if (align === "center") position[0] += 0.5 - scale[0] * 0.5; 36 | if (align === "middle") position[1] += 0.5 - scale[1] * 0.5; 37 | if (align === "bottom") position[1] += 1 - scale[1]; 38 | if (align === "right") position[0] += 1 - scale[0]; 39 | }); 40 | } 41 | 42 | if (props.repeat) { 43 | position = [dims * position[0] - dims * 0.5, dims * position[1] - dims * 0.5]; 44 | } else { 45 | position = [ctx.canvas.width * position[0] - dims * 0.5, ctx.canvas.height * position[1] - dims * 0.5]; 46 | ctx.scale(dims / ctx.canvas.width, dims / ctx.canvas.height); 47 | ctx.translate(dims * 0.5, dims * 0.5); 48 | ctx.rotate(rotation); 49 | ctx.translate(position[0], position[1]); 50 | ctx.scale(scale[0], scale[1]); 51 | } 52 | 53 | position = [position[0] + dims * 0.5, position[1] + dims * 0.5]; 54 | scale = [scale[0] * dims, scale[1] * dims]; 55 | 56 | if (callback) await callback({ position, scale, rotation }); 57 | ctx.resetTransform(); 58 | }; 59 | -------------------------------------------------------------------------------- /src/helpers/Random.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default class Random { 3 | _seedString: string = ""; 4 | seedSuffix: string = ""; 5 | _seedAdd: number = 0; 6 | _rnd: () => number = () => 0; 7 | 8 | constructor(seedSuffix = "foo", seedString = "") { 9 | this.seedSuffix = seedSuffix; 10 | this.seed(seedString); 11 | } 12 | 13 | _setSeed(str: string) { 14 | for (var i = 0, h = 1779033703 ^ str.length; i < str.length; i += 1) { 15 | (h = Math.imul(h ^ str.charCodeAt(i), 3432918353)), (h = (h << 13) | (h >>> 19)); 16 | } 17 | this._seedAdd = (function () { 18 | h = Math.imul(h ^ (h >>> 16), 2246822507); 19 | h = Math.imul(h ^ (h >>> 13), 3266489909); 20 | return (h ^= h >>> 16) >>> 0; 21 | })(); 22 | } 23 | 24 | getSeed() { 25 | return this._seedString; 26 | } 27 | 28 | seed(seedString: string) { 29 | this._seedString = `${seedString}_${this.seedSuffix}`; 30 | this._setSeed(this._seedString); 31 | this._rnd = () => { 32 | let t = (this._seedAdd += 0x6d2b79f5); 33 | t = Math.imul(t ^ (t >>> 15), t | 1); 34 | t ^= t + Math.imul(t ^ (t >>> 7), t | 61); 35 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296; 36 | }; 37 | return this; 38 | } 39 | 40 | float(minA = 1, maxA = 0) { 41 | const min = minA < maxA ? minA : maxA; 42 | const max = minA < maxA ? maxA : minA; 43 | return this._rnd() * (max - min) + min; 44 | } 45 | 46 | int(minA = 1, maxA = 0) { 47 | const min = minA < maxA ? minA : maxA; 48 | const max = minA < maxA ? maxA : minA; 49 | return Math.floor(this._rnd() * (max - min + 1) + min); 50 | } 51 | 52 | reset() { 53 | this._seedAdd = 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/helpers/flattenChildren.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const flattenChildren = (children: React.ReactNode) => { 4 | const childrenArray = Array.isArray(children) ? children : [children]; 5 | const layers: React.Component[] = childrenArray 6 | .map((child) => 7 | child?.props?.children 8 | ? flattenChildren(child.props.children) 9 | : typeof child?.type === "function" && child.type(child.props) 10 | ? flattenChildren(child.type(child.props)) 11 | : Array.isArray(child) 12 | ? flattenChildren(child) 13 | : child 14 | ) 15 | .flat() 16 | .filter((c) => c); 17 | return layers; 18 | }; 19 | 20 | export default flattenChildren; 21 | -------------------------------------------------------------------------------- /src/helpers/generatePMREM.ts: -------------------------------------------------------------------------------- 1 | import { EquirectangularReflectionMapping, PMREMGenerator, WebGLRenderer } from "three"; 2 | import { TextureResult } from "../types"; 3 | 4 | let pmremGenerator: PMREMGenerator; 5 | 6 | const generatePMREM = (gl: WebGLRenderer, tex: TextureResult) => { 7 | if (tex) { 8 | if (!pmremGenerator) { 9 | pmremGenerator = new PMREMGenerator(gl); 10 | pmremGenerator.compileEquirectangularShader(); 11 | tex = pmremGenerator.fromEquirectangular(tex).texture; 12 | } 13 | tex.mapping = EquirectangularReflectionMapping; 14 | pmremGenerator.dispose(); 15 | } 16 | return tex; 17 | }; 18 | 19 | export default generatePMREM; 20 | -------------------------------------------------------------------------------- /src/helpers/mixColors.ts: -------------------------------------------------------------------------------- 1 | export const mixColors = (c1: number[], c2: number[], factor: number): [number, number, number, number?] => { 2 | const [r1, g1, b1, a1] = c1; 3 | const [r2, g2, b2, a2] = c2; 4 | const mix = (start: number, end: number) => { 5 | return start + factor * (end - start); 6 | }; 7 | var red = Math.round(mix(r1, r2)); 8 | var green = Math.round(mix(g1, g2)); 9 | var blue = Math.round(mix(b1, b2)); 10 | var alpha = Math.round(mix(a1, a2)); 11 | return [red, green, blue, alpha]; 12 | }; 13 | -------------------------------------------------------------------------------- /src/helpers/newCanvasHelper.ts: -------------------------------------------------------------------------------- 1 | const newCanvasHelper = ( 2 | ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, 3 | callback: (ctxHelper: CanvasRenderingContext2D) => void, 4 | draw = true 5 | ) => { 6 | const ctxHelper = document.createElement("canvas").getContext("2d"); 7 | if (!ctxHelper) return; 8 | 9 | ctxHelper.canvas.width = ctx.canvas.width; 10 | ctxHelper.canvas.height = ctx.canvas.height; 11 | 12 | callback(ctxHelper); 13 | 14 | if (draw) ctx.drawImage(ctxHelper.canvas, 0, 0); //, ctxHelper.canvas.width, ctxHelper.canvas.height 15 | 16 | ctxHelper.canvas.remove(); 17 | }; 18 | 19 | export default newCanvasHelper; 20 | -------------------------------------------------------------------------------- /src/helpers/rgbToHex.ts: -------------------------------------------------------------------------------- 1 | export const rgbToHex = (red: number, green: number, blue: number, alpha: number = 1.0) => { 2 | var r = red & 0xff; 3 | var g = green & 0xff; 4 | var b = blue & 0xff; 5 | var a = Math.floor(alpha * 255) & 0xff; 6 | return a * 16777216 + b * 65536 + g * 256 + r; 7 | }; 8 | -------------------------------------------------------------------------------- /src/helpers/toUUID.ts: -------------------------------------------------------------------------------- 1 | const toUUID = function (props: object) { 2 | return Object.entries(props) 3 | .filter((_, v) => typeof v !== "undefined") 4 | .sort(([a], [b]) => (a < b ? -1 : 1)) 5 | .map(([key, value]) => { 6 | let newValue = value; 7 | if (typeof value === "object") { 8 | newValue = toUUID(value); 9 | } else if (typeof value === "number") { 10 | newValue = parseFloat(value.toFixed(4)); 11 | } 12 | return newValue ? [key, newValue] : null; 13 | }) 14 | .flat(Infinity) 15 | .filter((k) => k !== null) 16 | .join("-") 17 | .replace(/[\s\t]+/g, "-"); 18 | }; 19 | 20 | export default toUUID; 21 | -------------------------------------------------------------------------------- /src/helpers/wrapText.ts: -------------------------------------------------------------------------------- 1 | const wrapText = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, text: string, maxWidth: number) => { 2 | var words = text.split(" "); 3 | var lines = []; 4 | var currentLine = words[0]; 5 | 6 | for (var i = 1; i < words.length; i++) { 7 | var word = words[i]; 8 | var width = ctx.measureText(currentLine + " " + word).width; 9 | if (width < maxWidth) { 10 | currentLine += " " + word; 11 | } else { 12 | lines.push(currentLine); 13 | currentLine = word; 14 | } 15 | } 16 | lines.push(currentLine); 17 | return lines; 18 | }; 19 | 20 | export default wrapText; 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./components"; 2 | export * from "./setup"; 3 | export * from "./types"; 4 | -------------------------------------------------------------------------------- /src/polyfill/ctx.tsx: -------------------------------------------------------------------------------- 1 | export const ctxRoundRect = function ( 2 | ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, 3 | x: number, 4 | y: number, 5 | w: number, 6 | h: number, 7 | r: number 8 | ) { 9 | if (Math.abs(w) < 2 * r) r = Math.abs(w) / 2; 10 | if (Math.abs(h) < 2 * r) r = Math.abs(h) / 2; 11 | ctx.beginPath(); 12 | ctx.moveTo(x + r, y); 13 | ctx.arcTo(x + w, y, x + w, y + h, r); 14 | ctx.arcTo(x + w, y + h, x, y + h, r); 15 | ctx.arcTo(x, y + h, x, y, r); 16 | ctx.arcTo(x, y, x + w, y, r); 17 | ctx.closePath(); 18 | return ctx; 19 | }; 20 | 21 | type VectorProp = { 22 | x: number; 23 | y: number; 24 | nx?: number; 25 | ny?: number; 26 | radius?: number; 27 | len?: number; 28 | ang?: number; 29 | }; 30 | type VectorType = { 31 | x: number; 32 | y: number; 33 | nx: number; 34 | ny: number; 35 | radius: number; 36 | len: number; 37 | ang: number; 38 | }; 39 | 40 | // ctx is the context to add the path to 41 | // points is a array of points [{x :?, y: ?},... 42 | // radius is the max rounding radius 43 | // this creates a closed polygon. 44 | // To draw you must call between 45 | // ctx.beginPath(); 46 | // roundedPoly(ctx, points, radius); 47 | // ctx.stroke(); 48 | // ctx.fill(); 49 | // as it only adds a path and does not render. 50 | export const ctxRoundPoly2 = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, points: VectorProp[], radiusAll: number = 0) => { 51 | let i: number; 52 | let x: number; 53 | let y: number; 54 | let len: number; 55 | let p1: VectorType; 56 | let p2: VectorType; 57 | let p3: VectorType; 58 | let v1: VectorType; 59 | let v2: VectorType; 60 | let sinA: number; 61 | let sinA90: number; 62 | let radDirection: number; 63 | let drawDirection: boolean; 64 | let angle: number; 65 | let halfAngle: number; 66 | let cRadius: number; 67 | let lenOut: number; 68 | let radius: number; 69 | 70 | // convert 2 points into vector form, polar form, and normalised 71 | const asVec = function (p: VectorType, pp: VectorType, v: VectorType) { 72 | v.x = pp.x - p.x; 73 | v.y = pp.y - p.y; 74 | v.len = Math.sqrt(v.x * v.x + v.y * v.y); 75 | v.nx = v.x / v.len; 76 | v.ny = v.y / v.len; 77 | v.ang = Math.atan2(v.ny, v.nx); 78 | }; 79 | radius = radiusAll; 80 | v1 = { x: 0, y: 0, nx: 0, ny: 0, radius: 0, len: 0, ang: 0 }; 81 | v2 = { x: 0, y: 0, nx: 0, ny: 0, radius: 0, len: 0, ang: 0 }; 82 | len = points.length; 83 | p1 = points[len - 1] as VectorType; 84 | // for each point 85 | for (i = 0; i < len; i++) { 86 | p2 = points[i % len] as VectorType; 87 | p3 = points[(i + 1) % len] as VectorType; 88 | //----------------------------------------- 89 | // Part 1 90 | asVec(p2, p1, v1); 91 | asVec(p2, p3, v2); 92 | sinA = v1.nx * v2.ny - v1.ny * v2.nx; 93 | sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny; 94 | angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA); 95 | //----------------------------------------- 96 | radDirection = 1; 97 | drawDirection = false; 98 | if (sinA90 < 0) { 99 | if (angle < 0) { 100 | angle = Math.PI + angle; 101 | } else { 102 | angle = Math.PI - angle; 103 | radDirection = -1; 104 | drawDirection = true; 105 | } 106 | } else { 107 | if (angle > 0) { 108 | radDirection = -1; 109 | drawDirection = true; 110 | } 111 | } 112 | if (p2.radius !== undefined) { 113 | radius = p2.radius; 114 | } else { 115 | radius = radiusAll; 116 | } 117 | //----------------------------------------- 118 | // Part 2 119 | halfAngle = angle / 2; 120 | //----------------------------------------- 121 | 122 | //----------------------------------------- 123 | // Part 3 124 | lenOut = Math.abs((Math.cos(halfAngle) * radius) / Math.sin(halfAngle)); 125 | //----------------------------------------- 126 | 127 | //----------------------------------------- 128 | // Special part A 129 | if (lenOut > Math.min(v1.len / 2, v2.len / 2)) { 130 | lenOut = Math.min(v1.len / 2, v2.len / 2); 131 | cRadius = Math.abs((lenOut * Math.sin(halfAngle)) / Math.cos(halfAngle)); 132 | } else { 133 | cRadius = radius; 134 | } 135 | //----------------------------------------- 136 | // Part 4 137 | x = p2.x + v2.nx * lenOut; 138 | y = p2.y + v2.ny * lenOut; 139 | //----------------------------------------- 140 | // Part 5 141 | x += -v2.ny * cRadius * radDirection; 142 | y += v2.nx * cRadius * radDirection; 143 | //----------------------------------------- 144 | // Part 6 145 | ctx.arc(x, y, cRadius, v1.ang + (Math.PI / 2) * radDirection, v2.ang - (Math.PI / 2) * radDirection, drawDirection); 146 | //----------------------------------------- 147 | p1 = p2; 148 | p2 = p3; 149 | } 150 | ctx.closePath(); 151 | }; 152 | 153 | type VecProp = { 154 | x: number; 155 | y: number; 156 | }; 157 | export const ctxRoundPoly = (ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, points: VecProp[], radius: number) => { 158 | const distance = (p1: VecProp, p2: VecProp) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2); 159 | 160 | const lerp = (a: number, b: number, x: number) => a + (b - a) * x; 161 | 162 | const lerp2D = (p1: VecProp, p2: VecProp, t: number) => ({ 163 | x: lerp(p1.x, p2.x, t), 164 | y: lerp(p1.y, p2.y, t), 165 | }); 166 | 167 | const numPoints = points.length; 168 | 169 | let corners = []; 170 | for (let i = 0; i < numPoints; i++) { 171 | let lastPoint = points[i]; 172 | let thisPoint = points[(i + 1) % numPoints]; 173 | let nextPoint = points[(i + 2) % numPoints]; 174 | 175 | let lastEdgeLength = distance(lastPoint, thisPoint); 176 | let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius); 177 | let start = lerp2D(thisPoint, lastPoint, lastOffsetDistance / lastEdgeLength); 178 | 179 | let nextEdgeLength = distance(nextPoint, thisPoint); 180 | let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius); 181 | let end = lerp2D(thisPoint, nextPoint, nextOffsetDistance / nextEdgeLength); 182 | 183 | corners.push([start, thisPoint, end]); 184 | } 185 | 186 | ctx.moveTo(corners[0][0].x, corners[0][0].y); 187 | for (let [start, ctrl, end] of corners) { 188 | ctx.lineTo(start.x, start.y); 189 | ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y); 190 | } 191 | 192 | ctx.closePath(); 193 | }; 194 | -------------------------------------------------------------------------------- /src/setup.ts: -------------------------------------------------------------------------------- 1 | import { LayerProps } from "./types"; 2 | 3 | export const DEFAULT = { 4 | dimensions: 1024, 5 | src: "", 6 | image: "size-max center middle", 7 | position: [0, 0], 8 | scale: [1, 1], 9 | rotation: 0, 10 | color: "white", 11 | fill: "black", 12 | gradient: { 13 | type: "linear", 14 | linear: { 15 | from: [0, 0], 16 | to: [0, 1], 17 | stops: [ 18 | [0, "white"], 19 | [1, "black"], 20 | ], 21 | }, 22 | radial: { 23 | from: [0.5, 0.5, 0], 24 | to: [0.5, 0.5, 1], 25 | stops: [ 26 | [0, "white"], 27 | [1, "black"], 28 | ], 29 | }, 30 | }, 31 | nearest: false, 32 | shadow: { color: "black", blur: 20, offset: [0, 0] }, 33 | outline: { color: "black", size: 1, detail: 8 }, 34 | filter: "none", 35 | blend: "source-over", 36 | alpha: { level: 1, power: 1, offset: 0, reverse: false }, 37 | bloom: { size: 30, strength: 0.4, softness: 0.7, detail: 10, darken: false }, 38 | shapeThickness: 0, 39 | text: { 40 | font: "serif", 41 | style: "", 42 | weight: "", 43 | width: 0, 44 | height: 1.3, 45 | align: "center", 46 | base: "middle", 47 | }, 48 | seamless: { 49 | offset: [0.3, 0.3], 50 | size: [0.2, 0.2], 51 | both: false, 52 | alphaOffset: 0, 53 | alphaReverse: false, 54 | flipX: false, 55 | flipY: false, 56 | }, 57 | } as const; 58 | 59 | export let textureGlobals: LayerProps = { 60 | dimensions: undefined, 61 | src: undefined, 62 | fit: undefined, 63 | position: undefined, 64 | scale: undefined, 65 | rotation: undefined, 66 | color: undefined, 67 | fill: undefined, 68 | gradient: undefined, 69 | nearest: DEFAULT.nearest, 70 | shadow: undefined, 71 | outline: undefined, 72 | filter: undefined, 73 | blend: undefined, 74 | alpha: undefined, 75 | bloom: undefined, 76 | seamless: undefined, 77 | }; 78 | 79 | export const textureDefaults = (props: LayerProps) => { 80 | Object.assign(textureGlobals, props); 81 | }; 82 | -------------------------------------------------------------------------------- /src/storage/storage.ts: -------------------------------------------------------------------------------- 1 | import { CanvasTexture } from "three"; 2 | 3 | type KeyProps = Record; 4 | 5 | type Key = "IMG" | "LAY" | "TEX" | "FON"; 6 | 7 | type StoredObject = T extends "IMG" 8 | ? HTMLImageElement 9 | : T extends "LAY" 10 | ? HTMLCanvasElement 11 | : T extends "FON" 12 | ? FontFace 13 | : CanvasRenderingContext2D; 14 | 15 | type StorageRet = { 16 | get: () => StoredObject; 17 | set: (layer?: StoredObject) => void; 18 | }; 19 | 20 | const STORE = { 21 | IMG: {} as KeyProps>, 22 | LAY: {} as KeyProps>, 23 | TEX: {} as KeyProps>, 24 | FON: {} as KeyProps>, 25 | }; 26 | 27 | const storage = (key: T, name: string = ""): StorageRet => ({ 28 | get: () => STORE[key][name] as any, 29 | set: (layer) => { 30 | if (typeof layer !== "undefined") STORE[key][name] = layer; 31 | }, 32 | }); 33 | 34 | export default storage; 35 | -------------------------------------------------------------------------------- /src/types/Layer.ts: -------------------------------------------------------------------------------- 1 | export type GradientLinearProps = { 2 | type?: "linear"; 3 | from?: [number, number]; 4 | to?: [number, number]; 5 | stops?: [number, string][]; 6 | }; 7 | 8 | export type GradientRadialProps = { 9 | type?: "radial"; 10 | from?: [number, number, number]; 11 | to?: [number, number, number]; 12 | stops?: [number, string][]; 13 | }; 14 | 15 | export type ImageProps = { 16 | src?: string; 17 | fit?: string | boolean; 18 | }; 19 | 20 | export type TransformationProps = { 21 | position?: [number, number]; 22 | scale?: [number, number]; 23 | rotation?: number; 24 | }; 25 | 26 | export type ColorProps = { 27 | color?: string | boolean; 28 | fill?: string | boolean; 29 | }; 30 | 31 | export type ShadowProps = { 32 | color?: string; 33 | blur?: number; 34 | offset?: [number, number]; 35 | }; 36 | 37 | export type OutlineProps = { 38 | color?: string; 39 | size?: number; 40 | detail?: number; 41 | }; 42 | 43 | export type AlphaProps = { 44 | level?: number; 45 | power?: number; 46 | offset?: number; 47 | reverse?: boolean; 48 | }; 49 | 50 | export type BloomProps = { 51 | size?: number; 52 | strength?: number; 53 | softness?: number; 54 | detail?: number; 55 | darken?: boolean; 56 | }; 57 | 58 | export type TextProps = { 59 | value: string; 60 | font?: string; 61 | style?: string; 62 | weight?: string | number; 63 | width?: number; 64 | height?: number; 65 | align?: CanvasTextAlign; 66 | base?: CanvasTextBaseline; 67 | }; 68 | 69 | export type ShapeProps = { 70 | shapeThickness?: number; 71 | shapeRounded?: boolean; 72 | line?: [number, number, number, number, ...number[]]; 73 | circle?: [number, number, number, number?, number?, number?, number?, boolean?]; 74 | rect?: [number, number, number, number?, number?]; 75 | poly?: [...number[], number]; 76 | curve?: [number, number, number, number, number, number, number?, number?]; 77 | text?: TextProps; 78 | }; 79 | 80 | export type SeamlessProps = { 81 | offset?: [number, number]; 82 | size?: [number, number]; 83 | both?: boolean; 84 | flipX?: boolean; 85 | flipY?: boolean; 86 | alphaOffset?: number; 87 | alphaReverse?: boolean; 88 | }; 89 | 90 | export type NoiseRandomProps = { 91 | type: "random" | "perlin"; 92 | from?: string; 93 | to?: string; 94 | steps?: number; 95 | seed?: string | number; 96 | detail?: number; 97 | offset?: number; 98 | }; 99 | 100 | export type BricksProps = { 101 | color?: string | string[]; 102 | width?: number; 103 | height?: number; 104 | thickness?: number; 105 | offsetX?: number; 106 | seed?: number; 107 | randomize?: [number, number?, number?]; 108 | radius?: number; 109 | layer?: LayerProps; 110 | }; 111 | 112 | export interface LayerProps extends ImageProps, TransformationProps, ColorProps, ShapeProps { 113 | dimensions?: number; 114 | gradient?: GradientLinearProps | GradientRadialProps | boolean; 115 | nearest?: boolean; 116 | repeat?: boolean; 117 | shadow?: ShadowProps | boolean; 118 | outline?: OutlineProps | boolean; 119 | filter?: string; 120 | flipX?: boolean; 121 | flipY?: boolean; 122 | blend?: GlobalCompositeOperation; 123 | alpha?: AlphaProps | number | boolean; 124 | bloom?: BloomProps | boolean; 125 | seamless?: SeamlessProps | boolean; 126 | noise?: NoiseRandomProps; 127 | } 128 | -------------------------------------------------------------------------------- /src/types/TextureSet.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import { CanvasTexture, Texture } from "three"; 3 | 4 | export type TransformReturn = { 5 | position: [number, number]; 6 | scale: [number, number]; 7 | rotation: number; 8 | }; 9 | 10 | export type MapType = 11 | | "env" 12 | | "specular" 13 | | "displacement" 14 | | "normal" 15 | | "bump" 16 | | "roughness" 17 | | "metalness" 18 | | "alpha" 19 | | "light" 20 | | "emissive" 21 | | "clearcoat" 22 | | "clearcoatNormal" 23 | | "clearcoatRoughness" 24 | | "sheenRoughness" 25 | | "sheenColor" 26 | | "specularIntensity" 27 | | "specularColor" 28 | | "thickness" 29 | | "transmission" 30 | | "ao"; 31 | 32 | export interface TextureSetProps extends Record { 33 | map?: MapType; 34 | dimensions?: number; 35 | children?: ReactNode; 36 | } 37 | 38 | export type TextureResult = CanvasTexture | Texture | null; 39 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Layer"; 2 | export * from "./TextureSet"; 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Default 4 | "target": "es5", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | 10 | // Added 11 | "jsx": "react", 12 | "module": "ESNext", 13 | "declaration": true, 14 | "declarationDir": "types", 15 | "sourceMap": true, 16 | "outDir": "dist", 17 | "moduleResolution": "node", 18 | "allowSyntheticDefaultImports": true, 19 | "emitDeclarationOnly": true, 20 | "noImplicitAny": true 21 | }, 22 | "exclude": ["examples"] 23 | } 24 | --------------------------------------------------------------------------------