├── preview.png ├── js ├── shape │ ├── sample.js │ ├── bounds.js │ ├── shapeCylinder.js │ ├── shapeHeightmap.js │ └── shapeCone.js ├── layer.js ├── loader.js ├── lighting.js ├── styleUtils.js ├── color.js ├── gradient.js ├── renderer │ ├── rendererCanvas.js │ ├── renderer.js │ ├── rendererCSS.js │ └── rendererWebGL.js ├── plan │ ├── plan.js │ ├── trees.js │ ├── village.js │ └── heightmap.js ├── vector3.js ├── bufferedCubicNoise.js ├── island.js ├── shapes.js ├── main.js └── myr.js ├── README.md ├── LICENSE ├── css └── style.css └── index.html /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jobtalle/SketchIsland/HEAD/preview.png -------------------------------------------------------------------------------- /js/shape/sample.js: -------------------------------------------------------------------------------- 1 | const Sample = function(color, normal) { 2 | this.color = color; 3 | this.normal = normal; 4 | }; -------------------------------------------------------------------------------- /js/layer.js: -------------------------------------------------------------------------------- 1 | const Layer = function(x, y, canvas) { 2 | this.x = x; 3 | this.y = y; 4 | this.canvas = canvas; 5 | }; -------------------------------------------------------------------------------- /js/loader.js: -------------------------------------------------------------------------------- 1 | const Loader = function(element) { 2 | const loaded = document.getElementById("loaded"); 3 | 4 | this.update = status => { 5 | loaded.style.width = (status * 100).toFixed(2) + "%"; 6 | }; 7 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Island 2 | [Works in your browser!](https://jobtalle.com/SketchIsland/) 3 | 4 | [This blog post](https://jobtalle.com/layered_voxel_rendering.html) explains the technique. 5 | 6 | Visit [my website](https://jobtalle.com/sketches.html) for more sketches. 7 | 8 | ![alt text](preview.png "Island") 9 | -------------------------------------------------------------------------------- /js/lighting.js: -------------------------------------------------------------------------------- 1 | const Lighting = function() { 2 | const ambient = 0.75; 3 | const angle = new Vector3(1, -1, 2.5).normalize(); 4 | 5 | this.get = normal => ambient + 2 * (1 - ambient) * Math.max(0, normal.dot(angle)); 6 | this.getAmbient = (normal, ambient) => ambient + 2 * (1 - ambient) * Math.max(0, normal.dot(angle)); 7 | }; -------------------------------------------------------------------------------- /js/styleUtils.js: -------------------------------------------------------------------------------- 1 | const StyleUtils = { 2 | getVariable: function(name) { 3 | return getComputedStyle(document.body).getPropertyValue(name); 4 | }, 5 | 6 | getColor: function(name) { 7 | return Color.fromHex( 8 | StyleUtils.getVariable(name).toUpperCase().replace("#", "").replace(" ", "")); 9 | } 10 | }; -------------------------------------------------------------------------------- /js/color.js: -------------------------------------------------------------------------------- 1 | const Color = function(r, g, b, a = 1) { 2 | this.r = r; 3 | this.g = g; 4 | this.b = b; 5 | this.a = a; 6 | }; 7 | 8 | Color.fromHex = hex => { 9 | const integer = parseInt(hex, 16); 10 | 11 | if (hex.length === 6) 12 | return new Color( 13 | ((integer >> 16) & 0xFF) / 255, 14 | ((integer >> 8) & 0xFF) / 255, 15 | (integer & 0xFF) / 255); 16 | 17 | return new Color( 18 | ((integer >> 24) & 0xFF) / 255, 19 | ((integer >> 16) & 0xFF) / 255, 20 | ((integer >> 8) & 0xFF) / 255, 21 | (integer & 0xFF) / 255); 22 | }; -------------------------------------------------------------------------------- /js/shape/bounds.js: -------------------------------------------------------------------------------- 1 | const Bounds = function(start, end) { 2 | this.start = start; 3 | this.end = end; 4 | }; 5 | 6 | Bounds.prototype.contains = function(x, y, z) { 7 | return x >= this.start.x && x <= this.end.x && y >= this.start.y && y < this.end.y && z >= this.start.z && z < this.end.z; 8 | }; 9 | 10 | Bounds.prototype.overlaps = function(other) { 11 | if (this.start.x > other.end.x || other.start.x > this.end.x) 12 | return false; 13 | 14 | if (this.start.y > other.end.y || other.start.y > this.end.y) 15 | return false; 16 | 17 | return !(this.start.z > other.end.z || other.start.z > this.end.z); 18 | }; -------------------------------------------------------------------------------- /js/gradient.js: -------------------------------------------------------------------------------- 1 | const Gradient = function(stops) { 2 | this.sample = at => { 3 | let lastIndex = 0; 4 | 5 | while (stops[lastIndex].at <= Math.min(0.9999, at)) 6 | ++lastIndex; 7 | 8 | const first = stops[lastIndex - 1]; 9 | const last = stops[lastIndex]; 10 | const factor = (at - first.at) / (last.at - first.at); 11 | 12 | return new Color( 13 | first.color.r * (1 - factor) + last.color.r * factor, 14 | first.color.g * (1 - factor) + last.color.g * factor, 15 | first.color.b * (1 - factor) + last.color.b * factor, 16 | first.color.a * (1 - factor) + last.color.a * factor); 17 | }; 18 | }; 19 | 20 | Gradient.Stop = function(at, color) { 21 | this.at = at; 22 | this.color = color; 23 | }; -------------------------------------------------------------------------------- /js/shape/shapeCylinder.js: -------------------------------------------------------------------------------- 1 | const ShapeCylinder = function(origin, radius, height, color, density = 1) { 2 | this.bounds = new Bounds( 3 | new Vector3( 4 | Math.floor(origin.x - radius), 5 | Math.floor(origin.y - radius), 6 | Math.floor(origin.z)), 7 | new Vector3( 8 | Math.ceil(origin.x + radius), 9 | Math.ceil(origin.y + radius), 10 | Math.ceil(origin.z + height))); 11 | 12 | this.sample = (x, y, z) => { 13 | if (density !== 1 && density < Math.random()) 14 | return null; 15 | 16 | const dx = x - origin.x; 17 | const dy = y - origin.y; 18 | const distSquared = dx * dx + dy * dy; 19 | 20 | if (distSquared > radius * radius) 21 | return null; 22 | 23 | if (distSquared === 0) 24 | return new Sample( 25 | color, 26 | ShapeCylinder.NORMAL_CENTER); 27 | 28 | return new Sample( 29 | color, 30 | new Vector3(dx, dy, 0).normalize()); 31 | }; 32 | }; 33 | 34 | ShapeCylinder.NORMAL_CENTER = new Vector3(1, 0, 0); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Job Talle 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 | -------------------------------------------------------------------------------- /js/shape/shapeHeightmap.js: -------------------------------------------------------------------------------- 1 | const ShapeHeightmap = function(heightmap, height) { 2 | const bufferedColors = new Array(heightmap.getSize() * heightmap.getSize()); 3 | 4 | this.bounds = heightmap.getBounds(height); 5 | 6 | this.sample = (x, y, z) => { 7 | const h = heightmap.getHeight(x, y) * height; 8 | 9 | if (z >= h) 10 | return null; 11 | 12 | const index = x + y * heightmap.getSize(); 13 | 14 | if (!bufferedColors[index]) { 15 | if (heightmap.getType(x, y) === Heightmap.TYPE_DEFAULT) 16 | bufferedColors[index] = Heightmap.GRADIENTS[Heightmap.TYPE_DEFAULT].sample( 17 | Math.pow( 18 | heightmap.getHeight(x, y), 19 | Math.pow(heightmap.getNormal(x, y).dot(Vector3.UP), ShapeHeightmap.GRADIENT_POWER))); 20 | else 21 | bufferedColors[index] = Heightmap.GRADIENTS[Heightmap.TYPE_VOLCANO].sample(heightmap.getHeight(x, y)); 22 | } 23 | 24 | return new Sample( 25 | bufferedColors[index], 26 | heightmap.getNormal(x, y)); 27 | }; 28 | }; 29 | 30 | ShapeHeightmap.GRADIENT_POWER = 0.35; -------------------------------------------------------------------------------- /js/renderer/rendererCanvas.js: -------------------------------------------------------------------------------- 1 | const RendererCanvas = function(island, canvas) { 2 | this.setIsland = newIsland => { 3 | island = newIsland; 4 | }; 5 | 6 | this.clean = () => { 7 | canvas.getContext("2d").clearRect(0, 0, canvas.width, canvas.height); 8 | }; 9 | 10 | this.resize = () => { 11 | 12 | }; 13 | 14 | this.render = (angle, pitch, scale) => { 15 | const context = canvas.getContext("2d"); 16 | 17 | context.imageSmoothingEnabled = true; 18 | context.clearRect(0, 0, canvas.width, canvas.height); 19 | context.save(); 20 | context.translate(canvas.width * 0.5, canvas.height * 0.5); 21 | 22 | for (let z = 0; z < island.getLayers().length; ++z) { 23 | const layer = island.getLayers()[z]; 24 | 25 | context.save(); 26 | context.translate(0, (island.getPlan().getHeight() * 0.5 - z) * scale); 27 | context.scale(1, pitch); 28 | context.rotate(angle); 29 | context.scale(scale, scale); 30 | context.drawImage( 31 | layer.canvas, 32 | island.getPlan().getSize() * -0.5 + layer.x, 33 | island.getPlan().getSize() * -0.5 + layer.y); 34 | context.restore(); 35 | } 36 | 37 | context.restore(); 38 | }; 39 | }; -------------------------------------------------------------------------------- /js/shape/shapeCone.js: -------------------------------------------------------------------------------- 1 | const ShapeCone = function(origin, radius, height, color, density = 1) { 2 | const angle = Math.atan2(height, radius); 3 | const nz = Math.cos(angle); 4 | const fh = Math.sin(angle); 5 | 6 | this.bounds = new Bounds( 7 | new Vector3( 8 | Math.floor(origin.x - radius), 9 | Math.floor(origin.y - radius), 10 | Math.floor(origin.z)), 11 | new Vector3( 12 | Math.ceil(origin.x + radius), 13 | Math.ceil(origin.y + radius), 14 | Math.ceil(origin.z + height))); 15 | 16 | this.sample = (x, y, z) => { 17 | if (density !== 1 && density < Math.random()) 18 | return null; 19 | 20 | const dx = x - origin.x; 21 | const dy = y - origin.y; 22 | const dz = z - origin.z; 23 | const distSquared = dx * dx + dy * dy; 24 | const r = radius * (1 - dz / height); 25 | 26 | if (distSquared > r * r) 27 | return null; 28 | 29 | if (distSquared === 0) 30 | return new Sample( 31 | color, 32 | ShapeCone.NORMAL_CENTER); 33 | 34 | return new Sample( 35 | color, 36 | new Vector3(dx * fh, dy * fh, nz).normalize()); 37 | }; 38 | }; 39 | 40 | ShapeCone.NORMAL_CENTER = new Vector3(1, 0, 0); -------------------------------------------------------------------------------- /js/plan/plan.js: -------------------------------------------------------------------------------- 1 | const Plan = function(size, height, scale, lighting) { 2 | let heightmap = null; 3 | let shapes = null; 4 | let shapeHeightmap = null; 5 | let step = 0; 6 | let ready = false; 7 | let firstLoadFrame = true; 8 | 9 | const steps = [ 10 | () => { 11 | heightmap = new Heightmap(size); 12 | }, 13 | () => { 14 | shapes = new Shapes(size, height); 15 | shapeHeightmap = new ShapeHeightmap(heightmap, height); 16 | }, 17 | () => { 18 | new Trees(height, heightmap, shapeHeightmap.bounds, lighting, scale).plant(shapes); 19 | }, 20 | () => { 21 | new Village(height, heightmap, shapeHeightmap.bounds, scale).place(shapes); 22 | }, 23 | () => { 24 | shapes.add(shapeHeightmap); 25 | } 26 | ]; 27 | 28 | this.isReady = () => ready; 29 | 30 | this.generate = maxRate => { 31 | if (firstLoadFrame) { 32 | firstLoadFrame = false; 33 | 34 | return 0; 35 | } 36 | 37 | const startTime = new Date(); 38 | 39 | while ((new Date() - startTime) * 0.001 < maxRate && step < steps.length) 40 | steps[step++](); 41 | 42 | if (step === steps.length) 43 | ready = true; 44 | 45 | return step / steps.length; 46 | }; 47 | 48 | this.getSize = () => size; 49 | this.getHeight = () => height; 50 | this.getShapes = () => shapes; 51 | }; -------------------------------------------------------------------------------- /js/vector3.js: -------------------------------------------------------------------------------- 1 | const Vector3 = function(x, y, z) { 2 | this.x = x; 3 | this.y = y; 4 | this.z = z; 5 | }; 6 | 7 | Vector3.UP = new Vector3(0, 0, 1); 8 | 9 | Vector3.prototype.dot = function(other) { 10 | return this.x * other.x + this.y * other.y + this.z * other.z; 11 | }; 12 | 13 | Vector3.prototype.length = function() { 14 | return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z); 15 | }; 16 | 17 | Vector3.prototype.multiply = function(scalar) { 18 | this.x *= scalar; 19 | this.y *= scalar; 20 | this.z *= scalar; 21 | 22 | return this; 23 | }; 24 | 25 | Vector3.prototype.divide = function(scalar) { 26 | return this.multiply(1 / scalar); 27 | }; 28 | 29 | Vector3.prototype.normalize = function() { 30 | return this.divide(this.length()); 31 | }; 32 | 33 | Vector3.prototype.add = function(vector) { 34 | this.x += vector.x; 35 | this.y += vector.y; 36 | this.z += vector.z; 37 | 38 | return this; 39 | }; 40 | 41 | Vector3.prototype.subtract = function(vector) { 42 | this.x -= vector.x; 43 | this.y -= vector.y; 44 | this.z -= vector.z; 45 | 46 | return this; 47 | }; 48 | 49 | Vector3.prototype.copy = function() { 50 | return new Vector3(this.x, this.y, this.z); 51 | }; 52 | 53 | Vector3.prototype.cross = function(vector) { 54 | return new Vector3( 55 | this.y * vector.z - this.z * vector.y, 56 | this.z * vector.x - this.x * vector.z, 57 | this.x * vector.y - this.y * vector.x); 58 | }; -------------------------------------------------------------------------------- /js/renderer/renderer.js: -------------------------------------------------------------------------------- 1 | const Renderer = function(canvas2d, canvas3d, element) { 2 | let island = null; 3 | let current = null; 4 | let type = Renderer.TYPE_DEFAULT; 5 | 6 | this.supportsWebGL = canvas3d.getContext("webgl2") !== null; 7 | 8 | const instantiate = () => { 9 | if (current) 10 | current.clean(); 11 | 12 | switch (type) { 13 | case Renderer.TYPE_CANVAS: 14 | current = new RendererCanvas(island, canvas2d); 15 | 16 | break; 17 | case Renderer.TYPE_CSS: 18 | current = new RendererCSS(island, element); 19 | 20 | break; 21 | case Renderer.TYPE_WEBGL: 22 | current = new RendererWebGL(island, canvasWebgl); 23 | 24 | break; 25 | } 26 | }; 27 | 28 | this.setType = typeName => { 29 | type = typeName; 30 | 31 | instantiate(); 32 | }; 33 | 34 | this.update = newIsland => { 35 | island = newIsland; 36 | 37 | if (!current) 38 | instantiate(); 39 | 40 | current.setIsland(island); 41 | }; 42 | 43 | this.resize = (width, height) => { 44 | if (current) 45 | current.resize(width, height); 46 | }; 47 | 48 | this.render = (angle, pitch, scale) => { 49 | current.render(angle, pitch, scale); 50 | }; 51 | }; 52 | 53 | Renderer.TYPE_CANVAS = "canvas"; 54 | Renderer.TYPE_CSS = "css"; 55 | Renderer.TYPE_WEBGL = "webgl"; 56 | Renderer.TYPE_DEFAULT = Renderer.TYPE_CANVAS; -------------------------------------------------------------------------------- /js/bufferedCubicNoise.js: -------------------------------------------------------------------------------- 1 | const BufferedCubicNoise = function(width, height) { 2 | this.width = width; 3 | this.values = new Array((width + 2) * (height + 2)); 4 | 5 | for (let i = 0; i < this.values.length; ++i) 6 | this.values[i] = Math.random(); 7 | }; 8 | 9 | BufferedCubicNoise.prototype.interpolate = function(a, b, c, d, x) { 10 | const p = (d - c) - (a - b); 11 | 12 | return x * (x * (x * p + ((a - b) - p)) + (c - a)) + b; 13 | }; 14 | 15 | BufferedCubicNoise.prototype.sample = function(x, y) { 16 | const xi = Math.floor(x); 17 | const yi = Math.floor(y); 18 | 19 | return this.interpolate( 20 | this.interpolate( 21 | this.values[yi * this.width + xi], 22 | this.values[yi * this.width + xi + 1], 23 | this.values[yi * this.width + xi + 2], 24 | this.values[yi * this.width + xi + 3], 25 | x - xi), 26 | this.interpolate( 27 | this.values[(yi + 1) * this.width + xi], 28 | this.values[(yi + 1) * this.width + xi + 1], 29 | this.values[(yi + 1) * this.width + xi + 2], 30 | this.values[(yi + 1) * this.width + xi + 3], 31 | x - xi), 32 | this.interpolate( 33 | this.values[(yi + 2) * this.width + xi], 34 | this.values[(yi + 2) * this.width + xi + 1], 35 | this.values[(yi + 2) * this.width + xi + 2], 36 | this.values[(yi + 2) * this.width + xi + 3], 37 | x - xi), 38 | this.interpolate( 39 | this.values[(yi + 3) * this.width + xi], 40 | this.values[(yi + 3) * this.width + xi + 1], 41 | this.values[(yi + 3) * this.width + xi + 2], 42 | this.values[(yi + 3) * this.width + xi + 3], 43 | x - xi), 44 | y - yi) * 0.5 + 0.25; 45 | }; -------------------------------------------------------------------------------- /js/renderer/rendererCSS.js: -------------------------------------------------------------------------------- 1 | const RendererCSS = function(island, element) { 2 | let container = null; 3 | let slices, origins; 4 | 5 | const makeSlice = (layer, z) => { 6 | layer.canvas.className = RendererCSS.CLASS_SLICE; 7 | layer.canvas.style.top = (island.getPlan().getHeight() * 0.5 - z) + "px"; 8 | layer.canvas.style.transform = "scale(0)"; 9 | 10 | return layer.canvas; 11 | }; 12 | 13 | const make = () => { 14 | this.clean(); 15 | 16 | if (island === null) 17 | return; 18 | 19 | container = document.createElement("div"); 20 | container.id = RendererCSS.ID_CONTAINER; 21 | 22 | slices = new Array(island.getLayers().length); 23 | origins = new Array(slices.length); 24 | 25 | for (let z = 0; z < island.getLayers().length; ++z) { 26 | container.appendChild(slices[z] = makeSlice(island.getLayers()[z], z)); 27 | 28 | origins[z] = "translate(" + island.getLayers()[z].x + "px," + island.getLayers()[z].y + "px)"; 29 | } 30 | 31 | element.appendChild(container); 32 | }; 33 | 34 | this.setIsland = newIsland => { 35 | island = newIsland; 36 | 37 | make(); 38 | }; 39 | 40 | this.clean = () => { 41 | if (container) 42 | element.removeChild(container); 43 | }; 44 | 45 | this.resize = () => { 46 | 47 | }; 48 | 49 | this.render = (angle, pitch, scale) => { 50 | const originOffset = Math.round(island.getPlan().getSize() * -0.5); 51 | const sliceTransform = "scale(1," + pitch + ") rotate(" + angle + "rad) translate(" + originOffset + "px," + originOffset + "px)"; 52 | 53 | container.style.transform = "translate( " + (element.clientWidth * 0.5) + "px," + (element.clientHeight * 0.5) + "px) scale(" + scale + ")"; 54 | 55 | for (let z = 0; z < island.getLayers().length; ++z) 56 | slices[z].style.transform = sliceTransform + origins[z]; 57 | }; 58 | 59 | make(); 60 | }; 61 | 62 | RendererCSS.ID_CONTAINER = "slice-container"; 63 | RendererCSS.CLASS_SLICE = "slice"; 64 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-ocean: #80c5ef; 3 | --color-ocean-distant: #336d97; 4 | --color-beach-start: #99ddeb00; 5 | --color-beach-end: #d3c78c; 6 | --color-grass-start: #66ae60; 7 | --color-grass-end: #374b2e; 8 | --color-mountain-start: #715239; 9 | --color-mountain-end: #3e352d; 10 | --color-volcano-surface: #b75a3b; 11 | --color-volcano-deep: #cd8941; 12 | --color-tree-pine: #477c50dd; 13 | --color-hut-base: #9f9f9f; 14 | --color-hut-walls: #dacdad; 15 | --color-hut-roof: #b37d53; 16 | --loader-size: 24px; 17 | } 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | #wrapper { 24 | width: 100vw; 25 | height: 100vh; 26 | overflow: hidden; 27 | background: radial-gradient( 28 | ellipse at bottom, 29 | var(--color-ocean) 0, 30 | var(--color-ocean-distant) 100%); 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | #wrapper #canvas-wrapper { 36 | flex-grow: 1; 37 | } 38 | 39 | #wrapper #canvas-wrapper #div-renderer { 40 | position: absolute; 41 | overflow: hidden; 42 | } 43 | 44 | #wrapper #canvas-wrapper #div-renderer #slice-container { 45 | transform-origin: left top; 46 | } 47 | 48 | #wrapper #canvas-wrapper #div-renderer #slice-container .slice { 49 | position: absolute; 50 | transform-origin: left top; 51 | } 52 | 53 | #wrapper #canvas-wrapper #renderer-2d { 54 | position: absolute; 55 | } 56 | 57 | #wrapper #canvas-wrapper #renderer-webgl { 58 | position: absolute; 59 | } 60 | 61 | #wrapper #loader { 62 | background-color: #585858; 63 | width: 100%; 64 | height: var(--loader-size); 65 | border-top: 1px solid var(--color-mountain-end); 66 | user-select: none; 67 | } 68 | 69 | #wrapper #loader #loaded { 70 | width: 0; 71 | height: 100%; 72 | background-color: var(--color-grass-start); 73 | border-right: 1px solid var(--color-mountain-end); 74 | } 75 | 76 | #wrapper #controls-wrapper { 77 | position: absolute; 78 | width: 100%; 79 | display: flex; 80 | flex-direction: row; 81 | user-select: none; 82 | } 83 | 84 | #wrapper #controls-wrapper #controls { 85 | display: flex; 86 | flex-direction: column; 87 | background-color: rgba(255, 255, 255, 0.62); 88 | padding: 8px; 89 | } 90 | 91 | #wrapper #controls-wrapper #controls select { 92 | width: 100%; 93 | } 94 | 95 | #wrapper #controls-wrapper #controls button { 96 | width: 100%; 97 | height: 32px; 98 | } -------------------------------------------------------------------------------- /js/renderer/rendererWebGL.js: -------------------------------------------------------------------------------- 1 | const RendererWebGL = function(island, canvas) { 2 | const myr = new Myr(canvas, false, true); 3 | const surfaces = []; 4 | 5 | const clear = () => { 6 | for (const surface of surfaces) 7 | surface.free(); 8 | 9 | surfaces.length = 0; 10 | 11 | myr.clear(); 12 | myr.flush(); 13 | }; 14 | 15 | const make = () => { 16 | clear(); 17 | 18 | if (island === null) 19 | return; 20 | 21 | for (let z = 0; z < island.getLayers().length; ++z) { 22 | const layer = island.getLayers()[z]; 23 | const context = layer.canvas.getContext("2d"); 24 | const data = context.getImageData(0, 0, layer.canvas.width, layer.canvas.height); 25 | 26 | for (let i = 0; i < data.data.length; i += 4) { 27 | const alpha = data.data[i + 3] / 255; 28 | 29 | data.data[i] = Math.round(data.data[i] * alpha); 30 | data.data[i + 1] = Math.round(data.data[i + 1] * alpha); 31 | data.data[i + 2] = Math.round(data.data[i + 2] * alpha); 32 | } 33 | 34 | surfaces.push(new myr.Surface(layer.canvas.width, layer.canvas.height, data.data, true, false)); 35 | } 36 | }; 37 | 38 | this.setIsland = newIsland => { 39 | island = newIsland; 40 | 41 | make(); 42 | }; 43 | 44 | this.clean = () => { 45 | clear(); 46 | 47 | myr.free(); 48 | }; 49 | 50 | this.resize = (width, height) => { 51 | myr.resize(width, height); 52 | }; 53 | 54 | this.render = (angle, pitch, scale) => { 55 | myr.clear(); 56 | myr.bind(); 57 | myr.push(); 58 | 59 | myr.translate(myr.getWidth() * 0.5, myr.getHeight() * 0.5); 60 | 61 | for (let z = 0; z < island.getLayers().length; ++z) { 62 | const layer = island.getLayers()[z]; 63 | 64 | myr.push(); 65 | myr.translate(0, (island.getPlan().getHeight() * 0.5 - z) * scale); 66 | myr.scale(1, pitch); 67 | myr.rotate(-angle); 68 | myr.scale(scale, scale); 69 | 70 | surfaces[z].draw( 71 | island.getPlan().getSize() * -0.5 + layer.x, 72 | island.getPlan().getSize() * -0.5 + layer.y); 73 | 74 | myr.pop(); 75 | } 76 | 77 | myr.pop(); 78 | myr.flush(); 79 | }; 80 | 81 | myr.setClearColor(new Myr.Color(0, 0, 0, 0)); 82 | 83 | make(); 84 | }; -------------------------------------------------------------------------------- /js/plan/trees.js: -------------------------------------------------------------------------------- 1 | const Trees = function(height, heightmap, bounds, lighting, scale) { 2 | this.plant = shapes => { 3 | const stride = Trees.SPACING * scale; 4 | 5 | for (let y = bounds.start.y; y < bounds.end.y - Trees.DISPLACEMENT * scale; y += stride) { 6 | for (let x = bounds.start.x; x < bounds.end.x - Trees.DISPLACEMENT * scale; x += stride) { 7 | const plantX = Math.round(x + Math.random() * Trees.DISPLACEMENT * scale); 8 | const plantY = Math.round(y + Math.random() * Trees.DISPLACEMENT * scale); 9 | const h = heightmap.getHeight(plantX, plantY); 10 | 11 | if (h < Trees.HEIGHT_MIN || h > Trees.HEIGHT_MAX) 12 | continue; 13 | 14 | let heightFactor; 15 | 16 | if (h < Trees.HEIGHT_PEAK) 17 | heightFactor = 1 - (h - Trees.HEIGHT_MIN) * (1 / (Trees.HEIGHT_PEAK - Trees.HEIGHT_MIN)); 18 | else 19 | heightFactor = (h - Trees.HEIGHT_PEAK) * (1 / (Trees.HEIGHT_MAX - Trees.HEIGHT_PEAK)); 20 | 21 | if (Math.random() < Trees.CHANCE_MIN * heightFactor) 22 | continue; 23 | 24 | if (heightmap.getNormal(plantX, plantY).dot(Trees.DIRECTION) < Trees.DOT_MIN) 25 | continue; 26 | 27 | const radius = (Trees.RADIUS_MIN + (Trees.RADIUS_MAX - Trees.RADIUS_MIN) * Math.random() * (1 - heightFactor)) * scale; 28 | const tall = radius * (Trees.HEIGHT_FACTOR_MIN + (Trees.HEIGHT_FACTOR_MAX - Trees.HEIGHT_FACTOR_MIN) * Math.random()); 29 | const l = lighting.getAmbient(heightmap.getNormal(plantX, plantY), Trees.AMBIENT); 30 | 31 | shapes.add(new ShapeCone(new Vector3(plantX, plantY, h * height - Trees.INSET), radius, tall, new Color( 32 | Math.max(0, Math.min(1, Trees.COLOR_PINE.r * l)), 33 | Math.max(0, Math.min(1, Trees.COLOR_PINE.g * l)), 34 | Math.max(0, Math.min(1, Trees.COLOR_PINE.b * l)), 35 | Trees.COLOR_PINE.a), 36 | 1 - (1 - Trees.VOLUME_DENSITY) * scale)); 37 | } 38 | } 39 | }; 40 | }; 41 | 42 | Trees.VOLUME_DENSITY = 0.5; 43 | Trees.RADIUS_MIN = 8; 44 | Trees.RADIUS_MAX = 18; 45 | Trees.HEIGHT_FACTOR_MIN = 1.8; 46 | Trees.HEIGHT_FACTOR_MAX = 2.5; 47 | Trees.SPACING = Trees.RADIUS_MIN * 1.65; 48 | Trees.DISPLACEMENT = Trees.SPACING; 49 | Trees.COLOR_PINE = StyleUtils.getColor("--color-tree-pine"); 50 | Trees.DIRECTION = new Vector3(-0.1, 0.1, 2).normalize(); 51 | Trees.DOT_MIN = 0.15; 52 | Trees.HEIGHT_MIN = 0.1; 53 | Trees.HEIGHT_PEAK = 0.2; 54 | Trees.HEIGHT_MAX = 0.65; 55 | Trees.CHANCE_MIN = 0.9; 56 | Trees.AMBIENT = 0.85; 57 | Trees.INSET = 1.5; -------------------------------------------------------------------------------- /js/island.js: -------------------------------------------------------------------------------- 1 | const Island = function(lighting) { 2 | let ready = false; 3 | let plan = null; 4 | let layers = null; 5 | let z; 6 | 7 | this.generate = maxRate => { 8 | const startTime = new Date(); 9 | const size = plan.getSize(); 10 | 11 | while ((new Date() - startTime) * 0.001 < maxRate) { 12 | let xMin = plan.getSize(); 13 | let xMax = 0; 14 | let yMin = plan.getSize(); 15 | let yMax = 0; 16 | 17 | const canvas = document.createElement("canvas"); 18 | const context = canvas.getContext("2d"); 19 | const data = context.createImageData(plan.getSize(), plan.getSize()); 20 | let index = 0; 21 | 22 | for (let y = 0; y < size; ++y) { 23 | let shapes = plan.getShapes().get(0, y, z); 24 | let shapesRefresh = Shapes.CELL_SIZE + 1; 25 | 26 | for (let x = 0; x < size; ++x) { 27 | if (--shapesRefresh === 0) { 28 | shapesRefresh = Shapes.CELL_SIZE; 29 | shapes = plan.getShapes().get(x, y, z); 30 | } 31 | 32 | for (let shape = 0; shape < shapes.length; ++shape) { 33 | if (shapes[shape].bounds.contains(x, y, z)) { 34 | const sample = shapes[shape].sample(x, y, z); 35 | 36 | if (!sample) 37 | continue; 38 | 39 | const l = lighting.get(sample.normal) * 255; 40 | 41 | data.data[index] = Math.min(Math.round(sample.color.r * l), 255); 42 | data.data[index + 1] = Math.min(Math.round(sample.color.g * l), 255); 43 | data.data[index + 2] = Math.min(Math.round(sample.color.b * l), 255); 44 | data.data[index + 3] = Math.round(sample.color.a * 255); 45 | 46 | if (x < xMin) 47 | xMin = x; 48 | 49 | if (y < yMin) 50 | yMin = y; 51 | 52 | if (x > xMax) 53 | xMax = x; 54 | 55 | if (y > yMax) 56 | yMax = y; 57 | 58 | break; 59 | } 60 | } 61 | 62 | index += 4; 63 | } 64 | } 65 | 66 | const width = xMax - xMin; 67 | const height = yMax - yMin; 68 | 69 | if (width > 0 && height > 0) { 70 | canvas.width = width; 71 | canvas.height = height; 72 | context.putImageData(data, -xMin, -yMin); 73 | 74 | layers.push(new Layer(xMin, yMin, canvas)); 75 | } 76 | else { 77 | z = plan.getHeight(); 78 | ready = true; 79 | 80 | break; 81 | } 82 | 83 | if (++z === plan.getHeight()) { 84 | ready = true; 85 | 86 | break; 87 | } 88 | } 89 | 90 | return z / plan.getHeight(); 91 | }; 92 | 93 | this.isReady = () => ready; 94 | this.getLayers = () => layers; 95 | this.getPlan = () => plan; 96 | 97 | this.setPlan = newPlan => { 98 | z = 0; 99 | ready = false; 100 | plan = newPlan; 101 | layers = []; 102 | }; 103 | }; -------------------------------------------------------------------------------- /js/shapes.js: -------------------------------------------------------------------------------- 1 | const Shapes = function(size, height) { 2 | const sizeCells = Math.ceil(size * Shapes.CELL_SIZE_INVERSE); 3 | const sizeCellsSquared = sizeCells * sizeCells; 4 | const heightCells = Math.ceil(height * Shapes.CELL_SIZE_INVERSE); 5 | const cells = new Array(sizeCellsSquared * heightCells); 6 | 7 | for (let i = 0; i < cells.length; ++i) 8 | cells[i] = []; 9 | 10 | this.cropBounds = bounds => { 11 | if (bounds.start.x < 0) 12 | bounds.start.x = 0; 13 | 14 | if (bounds.start.y < 0) 15 | bounds.start.y = 0; 16 | 17 | if (bounds.start.z < 0) 18 | bounds.start.z = 0; 19 | 20 | if (bounds.end.x > size) 21 | bounds.end.x = size; 22 | 23 | if (bounds.end.y > size) 24 | bounds.end.y = size; 25 | 26 | if (bounds.end.z > height) 27 | bounds.end.z = height; 28 | }; 29 | 30 | this.clear = bounds => { 31 | const shapes = []; 32 | 33 | for (let z = Math.floor(bounds.start.z * Shapes.CELL_SIZE_INVERSE); 34 | z < Math.ceil(bounds.end.z * Shapes.CELL_SIZE_INVERSE); 35 | ++z) 36 | for (let y = Math.floor(bounds.start.y * Shapes.CELL_SIZE_INVERSE); 37 | y < Math.ceil(bounds.end.y * Shapes.CELL_SIZE_INVERSE); 38 | ++y) 39 | for (let x = Math.floor(bounds.start.x * Shapes.CELL_SIZE_INVERSE); 40 | x < Math.ceil(bounds.end.x * Shapes.CELL_SIZE_INVERSE); 41 | ++x) 42 | for (const shape of cells[z * sizeCellsSquared + y * sizeCells + x]) 43 | if (shapes.indexOf(shape) === -1 && shape.bounds.overlaps(bounds)) 44 | shapes.push(shape); 45 | 46 | for (const shape of shapes) { 47 | for (let z = Math.floor(shape.bounds.start.z * Shapes.CELL_SIZE_INVERSE); 48 | z < Math.ceil(shape.bounds.end.z * Shapes.CELL_SIZE_INVERSE); 49 | ++z) { 50 | for (let y = Math.floor(shape.bounds.start.y * Shapes.CELL_SIZE_INVERSE); 51 | y < Math.ceil(shape.bounds.end.y * Shapes.CELL_SIZE_INVERSE); 52 | ++y) { 53 | for (let x = Math.floor(shape.bounds.start.x * Shapes.CELL_SIZE_INVERSE); 54 | x < Math.ceil(shape.bounds.end.x * Shapes.CELL_SIZE_INVERSE); 55 | ++x) { 56 | const cell = cells[z * sizeCellsSquared + y * sizeCells + x]; 57 | 58 | for (let i = cell.length; i-- > 0;) 59 | if (shapes.indexOf(cell[i]) !== -1) 60 | cell.splice(i, 1); 61 | } 62 | } 63 | } 64 | } 65 | }; 66 | 67 | this.add = shape => { 68 | this.cropBounds(shape.bounds); 69 | 70 | for (let z = Math.floor(shape.bounds.start.z * Shapes.CELL_SIZE_INVERSE); 71 | z < Math.ceil(shape.bounds.end.z * Shapes.CELL_SIZE_INVERSE); 72 | ++z) { 73 | for (let y = Math.floor(shape.bounds.start.y * Shapes.CELL_SIZE_INVERSE); 74 | y < Math.ceil(shape.bounds.end.y * Shapes.CELL_SIZE_INVERSE); 75 | ++y) { 76 | for (let x = Math.floor(shape.bounds.start.x * Shapes.CELL_SIZE_INVERSE); 77 | x < Math.ceil(shape.bounds.end.x * Shapes.CELL_SIZE_INVERSE); 78 | ++x) { 79 | cells[z * sizeCellsSquared + y * sizeCells + x].push(shape); 80 | } 81 | } 82 | } 83 | }; 84 | 85 | this.get = (x, y, z) => { 86 | return cells[ 87 | Math.floor(z * Shapes.CELL_SIZE_INVERSE) * sizeCellsSquared + 88 | Math.floor(y * Shapes.CELL_SIZE_INVERSE) * sizeCells + 89 | Math.floor(x * Shapes.CELL_SIZE_INVERSE)]; 90 | }; 91 | }; 92 | 93 | Shapes.CELL_SIZE = 24; 94 | Shapes.CELL_SIZE_INVERSE = 1 / Shapes.CELL_SIZE; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Island 11 | 12 | 13 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 39 | 40 | 41 | 42 | 51 | 52 | 53 | 54 | 57 | 58 |
Quality 29 | 38 |
Render mode 43 | 50 |
55 | 56 |
59 |
60 |
61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | const ANGLE_SPEED = 0.3; 2 | const DRAG_SPEED = 0.006; 3 | const X_FILL = 1; 4 | const Y_FILL = 0.65; 5 | const HEIGHT_RATIO = 0.18; 6 | const TIME_STEP_MAX = 0.1; 7 | const SIZE_MAX = 1500; 8 | const GEN_RATE = 1 / 60; 9 | const PLAN_PERCENTAGE = 0.1; 10 | 11 | const lighting = new Lighting(); 12 | const island = new Island(lighting); 13 | const loader = new Loader(document.getElementById("loader")); 14 | const canvasWrapper = document.getElementById("canvas-wrapper"); 15 | const canvasWebgl = document.getElementById("renderer-webgl"); 16 | const canvas2d = document.getElementById("renderer-2d"); 17 | const divRenderer = document.getElementById("div-renderer"); 18 | const renderer = new Renderer(canvas2d, canvasWebgl, divRenderer); 19 | let lastDate = new Date(); 20 | let plan = null; 21 | let size = 0; 22 | let height = 0; 23 | let angle = Math.PI * 0.5; 24 | let pitch = 0.4; 25 | let scale = 2; 26 | let angleDelta = 0; 27 | let updated = false; 28 | let dragging = false; 29 | let xDrag = 0; 30 | 31 | if (!renderer.supportsWebGL) { 32 | const webGlOption = document.getElementById("option-webgl"); 33 | 34 | webGlOption.parentElement.removeChild(webGlOption); 35 | } 36 | 37 | const updateParameters = () => { 38 | size = Math.min(SIZE_MAX, Math.min( 39 | Math.floor(canvas2d.width * X_FILL / scale), 40 | Math.floor((canvas2d.height * Y_FILL / pitch) / scale))); 41 | height = Math.ceil(size * HEIGHT_RATIO); 42 | updated = true; 43 | }; 44 | 45 | const resize = () => { 46 | canvas2d.width = canvasWebgl.width = canvasWrapper.offsetWidth; 47 | canvas2d.height = canvasWebgl.height = canvasWrapper.offsetHeight; 48 | divRenderer.style.width = canvas2d.width + "px"; 49 | divRenderer.style.height = canvas2d.height + "px"; 50 | 51 | renderer.resize(canvasWrapper.offsetWidth, canvasWrapper.offsetHeight); 52 | 53 | updateParameters(); 54 | }; 55 | 56 | const update = timeStep => { 57 | if (plan && !plan.isReady()) { 58 | loader.update(plan.generate(GEN_RATE) * PLAN_PERCENTAGE); 59 | 60 | if (plan.isReady()) { 61 | island.setPlan(plan); 62 | } 63 | } 64 | else if (!island.isReady()) { 65 | loader.update(PLAN_PERCENTAGE+ island.generate(GEN_RATE) * (1 - PLAN_PERCENTAGE)); 66 | 67 | if (island.isReady()) { 68 | updated = true; 69 | 70 | renderer.update(island); 71 | } 72 | } 73 | 74 | if (updated || (!dragging && angleDelta !== 0)) { 75 | if (!dragging && angleDelta !== 0) 76 | if ((angle += timeStep * angleDelta) > Math.PI + Math.PI) 77 | angle -= Math.PI + Math.PI; 78 | else if (angle < 0) 79 | angle += Math.PI + Math.PI; 80 | 81 | if (plan.isReady() && island.isReady()) 82 | renderer.render(angle, pitch, scale); 83 | 84 | updated = false; 85 | } 86 | }; 87 | 88 | const loopFunction = () => { 89 | const date = new Date(); 90 | 91 | update(Math.min(TIME_STEP_MAX, (date - lastDate) * 0.001)); 92 | requestAnimationFrame(loopFunction); 93 | 94 | lastDate = date; 95 | }; 96 | 97 | const replan = () => { 98 | loader.update(0); 99 | plan = new Plan(size, height, 1 / scale, lighting); 100 | }; 101 | 102 | const mouseDown = (x, y, drag) => { 103 | if (drag) { 104 | xDrag = x; 105 | dragging = true; 106 | angleDelta = 0; 107 | } 108 | else 109 | replan(); 110 | }; 111 | 112 | const mouseUp = touch => { 113 | dragging = false; 114 | 115 | if (updated && !touch) 116 | angleDelta = ANGLE_SPEED * Math.sign(angleDelta); 117 | else 118 | angleDelta = 0; 119 | }; 120 | 121 | const mouseMove = (x, y) => { 122 | if (dragging) { 123 | angleDelta = (xDrag - x) * DRAG_SPEED; 124 | angle += angleDelta; 125 | xDrag = x; 126 | updated = true; 127 | } 128 | }; 129 | 130 | window.onresize = resize; 131 | 132 | canvas2d.addEventListener("mousedown", event => { 133 | if (event.button === 0) 134 | mouseDown(event.clientX, event.clientY, true); 135 | else if (event.button === 1) 136 | mouseDown(event.clientX, event.clientY, false); 137 | }); 138 | canvas2d.addEventListener("touchstart", event => 139 | mouseDown(event.touches[0].clientX, event.touches[0].clientY, true)); 140 | canvas2d.addEventListener("mousemove", event => 141 | mouseMove(event.clientX, event.clientY)); 142 | canvas2d.addEventListener("touchmove", event => 143 | mouseMove(event.touches[0].clientX, event.touches[0].clientY)); 144 | canvas2d.addEventListener("mouseup", event => 145 | mouseUp(false)); 146 | canvas2d.addEventListener("touchend", event => 147 | mouseUp(true)); 148 | 149 | requestAnimationFrame(loopFunction); 150 | resize(); 151 | replan(); -------------------------------------------------------------------------------- /js/plan/village.js: -------------------------------------------------------------------------------- 1 | const Village = function(height, heightmap, bounds, scale) { 2 | const HutPlan = function(location) { 3 | this.base = new ShapeCylinder( 4 | location.copy().subtract(new Vector3(0, 0, Village.HUT_DEPTH * scale)), 5 | Village.HUT_RADIUS * Village.HUT_ROOF_RADIUS * scale, 6 | Village.HUT_DEPTH * scale, 7 | Village.COLOR_HUT_BASE); 8 | this.roof = new ShapeCone( 9 | location.copy().add(new Vector3(0, 0, Village.HUT_HEIGHT * scale)), 10 | Village.HUT_RADIUS * Village.HUT_ROOF_RADIUS * scale, 11 | Village.HUT_HEIGHT * Village.HUT_ROOF_HEIGHT * scale, 12 | Village.COLOR_HUT_ROOF); 13 | this.walls = new ShapeCylinder( 14 | location.copy(), 15 | Village.HUT_RADIUS * scale, 16 | Village.HUT_HEIGHT * scale, 17 | Village.COLOR_HUT_WALLS); 18 | }; 19 | 20 | const suitableVillage = (x, y) => { 21 | if (x < 0 || y < 0 || x >= heightmap.getSize() || y >= heightmap.getSize()) 22 | return false; 23 | 24 | const h = heightmap.getHeight(x, y); 25 | 26 | if (h < Village.VILLAGE_HEIGHT_MIN || h > Village.VILLAGE_HEIGHT_MAX) 27 | return false; 28 | 29 | return heightmap.getNormal(x, y).dot(Vector3.UP) >= Village.VILLAGE_DOT_MIN; 30 | }; 31 | 32 | const suitableHut = (x, y) => { 33 | if (x < 0 || y < 0 || x >= heightmap.getSize() || y >= heightmap.getSize()) 34 | return false; 35 | 36 | const h = heightmap.getHeight(x, y); 37 | 38 | if (h < Village.HUT_HEIGHT_MIN || h > Village.HUT_HEIGHT_MAX) 39 | return false; 40 | 41 | return heightmap.getNormal(x, y).dot(Vector3.UP) >= Village.HUT_DOT_MIN; 42 | }; 43 | 44 | const getSuitableLocations = () => { 45 | const Candidate = function(x, y, score) { 46 | this.x = x; 47 | this.y = y; 48 | this.score = score; 49 | }; 50 | 51 | const candidates = []; 52 | 53 | for (let y = bounds.start.y; y < bounds.end.y; y += Village.PROBE_STRIDE * scale) { 54 | for (let x = bounds.start.x; x < bounds.end.x; x += Village.PROBE_STRIDE * scale) { 55 | const xr = Math.round(x); 56 | const yr = Math.round(y); 57 | 58 | if (!suitableVillage(xr, yr)) 59 | continue; 60 | 61 | candidates.push(new Candidate(xr, yr, heightmap.getNormal(xr, yr).dot(Vector3.UP))); 62 | } 63 | } 64 | 65 | return candidates.sort((a, b) => b.score - a.score); 66 | }; 67 | 68 | const growVillage = (x, y, shapes) => { 69 | const plans = []; 70 | 71 | plans.push(new HutPlan(new Vector3(x, y, heightmap.getHeight(x, y) * height))); 72 | 73 | const spacingAverage = (Village.HUT_SPACING_MIN + Village.HUT_SPACING_MAX) * 0.5 * scale; 74 | const spacingDeviation = (Village.HUT_SPACING_MAX - Village.HUT_SPACING_MIN) * 0.5 * scale; 75 | const area = heightmap.getSize() * heightmap.getSize(); 76 | const hutCount = Math.round((Village.HUTS_MIN + (Village.HUTS_MAX - Village.HUTS_MIN) * Math.pow(Math.random(), Village.HUTS_POWER) * area)); 77 | let emptyRings = 0; 78 | 79 | for (let ring = 0; ring < Village.RINGS_MAX && plans.length < hutCount; ++ring) { 80 | const angleOffset = Math.random(); 81 | const huts = Math.floor(Math.PI * ring * Village.HUT_SPACING_MIN * 2 / spacingAverage); 82 | let placed = false; 83 | 84 | for (let i = 0; i < huts && plans.length < hutCount; ++i) { 85 | const radius = ring * spacingAverage - spacingDeviation + spacingDeviation * Math.random() * 2; 86 | const angle = Math.PI * 2 * (i + angleOffset) / huts; 87 | const hx = Math.round(x + Math.cos(angle) * radius); 88 | const hy = Math.round(y + Math.sin(angle) * radius); 89 | 90 | if (!suitableHut(hx, hy) || Math.random() < Village.HUT_SKIP_CHANCE) 91 | continue; 92 | 93 | plans.push(new HutPlan(new Vector3(hx, hy, heightmap.getHeight(hx, hy) * height))); 94 | placed = true; 95 | } 96 | 97 | if (!placed) { 98 | if (++emptyRings > Village.RINGS_EMPTY_MAX) 99 | return false; 100 | } 101 | else 102 | emptyRings = 0; 103 | } 104 | 105 | if (plans.length < Village.MIN_SIZE) 106 | return false; 107 | 108 | for (const plan of plans) { 109 | shapes.cropBounds(plan.roof.bounds); 110 | shapes.clear(plan.roof.bounds); 111 | } 112 | 113 | for (const plan of plans) { 114 | shapes.add(plan.roof); 115 | shapes.add(plan.walls); 116 | shapes.add(plan.base); 117 | } 118 | 119 | return true; 120 | }; 121 | 122 | this.place = shapes => { 123 | const locations = getSuitableLocations(); 124 | 125 | while (locations.length !== 0) { 126 | const location = locations.shift(); 127 | 128 | if (growVillage(location.x, location.y, shapes)) 129 | return; 130 | } 131 | }; 132 | }; 133 | 134 | Village.RINGS_MAX = 8; 135 | Village.RINGS_EMPTY_MAX = 1; 136 | Village.HUTS_MIN = 0.0025; 137 | Village.HUTS_MAX = 0.015; 138 | Village.HUTS_POWER = 2; 139 | Village.VILLAGE_HEIGHT_MIN = 0.1; 140 | Village.VILLAGE_HEIGHT_MAX = 0.15; 141 | Village.VILLAGE_DOT_MIN = 0.7; 142 | Village.HUT_SKIP_CHANCE = 0.15; 143 | Village.HUT_HEIGHT_MIN = 0.15; 144 | Village.HUT_HEIGHT_MAX = 0.5; 145 | Village.HUT_DOT_MIN = 0.45; 146 | Village.HUT_RADIUS = 9; 147 | Village.HUT_HEIGHT = Village.HUT_RADIUS * 1.2; 148 | Village.HUT_DEPTH = Village.HUT_HEIGHT; 149 | Village.HUT_ROOF_RADIUS = 1.3; 150 | Village.HUT_ROOF_HEIGHT = 1; 151 | Village.HUT_SPACING_MIN = Village.HUT_RADIUS * 2.5; 152 | Village.HUT_SPACING_MAX = Village.HUT_SPACING_MIN * 2; 153 | Village.MIN_SIZE = 3; 154 | Village.PROBE_STRIDE = 24; 155 | Village.COLOR_HUT_BASE = StyleUtils.getColor("--color-hut-base"); 156 | Village.COLOR_HUT_WALLS = StyleUtils.getColor("--color-hut-walls"); 157 | Village.COLOR_HUT_ROOF = StyleUtils.getColor("--color-hut-roof"); -------------------------------------------------------------------------------- /js/plan/heightmap.js: -------------------------------------------------------------------------------- 1 | const Heightmap = function(size) { 2 | const heights = new Array(size * size); 3 | const normals = new Array(heights.length); 4 | const types = new Array(heights.length); 5 | let xMin = size; 6 | let xMax = 0; 7 | let yMin = size; 8 | let yMax = 0; 9 | let zMax = 0; 10 | 11 | const calculateNormals = () => { 12 | const step = 1 / size; 13 | 14 | for (let y = 1; y < size - 1; ++y) for (let x = 1; x < size - 1; ++x) { 15 | const index = x + size * y; 16 | const left = new Vector3(-step, 0, heights[index - 1] - heights[index]); 17 | const right = new Vector3(step, 0, heights[index + 1] - heights[index]); 18 | const top = new Vector3(0, -step, heights[index - size] - heights[index]); 19 | const bottom = new Vector3(0, step, heights[index + size] - heights[index]); 20 | const normal = bottom.cross(left); 21 | 22 | normal.add(right.cross(bottom)); 23 | normal.add(top.cross(right)); 24 | normal.add(left.cross(top)); 25 | 26 | normals[index] = normal.normalize(); 27 | } 28 | 29 | for (let x = 0; x < size; ++x) 30 | normals[x] = normals[size * size - size + x] = Heightmap.NORMAL_EDGE; 31 | 32 | for (let y = 1; y < size - 1; ++y) 33 | normals[y * size] = normals[y * size + size - 1] = Heightmap.NORMAL_EDGE; 34 | }; 35 | 36 | const makeNoises = maxScale => { 37 | const noises = new Array(Heightmap.OCTAVES); 38 | let s = maxScale; 39 | 40 | for (let i = 0; i < Heightmap.OCTAVES; ++i) { 41 | noises[i] = new BufferedCubicNoise(Math.ceil(s * size), Math.ceil(s * size)); 42 | 43 | s *= Heightmap.SCALE_FALLOFF; 44 | } 45 | 46 | return noises; 47 | }; 48 | 49 | const fill = () => { 50 | const peakPower = Heightmap.PEAK_POWER_MIN + (Heightmap.PEAK_POWER_MAX - Heightmap.PEAK_POWER_MIN) * Math.random(); 51 | const maxScale = (1 / size) * (Heightmap.SCALE_MIN + (Heightmap.SCALE_MAX - Heightmap.SCALE_MIN) * Math.random()); 52 | const power = Heightmap.POWER_MIN + (Heightmap.POWER_MAX - Heightmap.POWER_MIN) * Math.random(); 53 | const waterThreshold = Heightmap.WATER_THRESHOLD_MIN + (Heightmap.WATER_THRESHOLD_MAX - Heightmap.WATER_THRESHOLD_MIN) * Math.random(); 54 | const noises = makeNoises(maxScale); 55 | 56 | for (let y = 0; y < size; ++y) for (let x = 0; x < size; ++x) { 57 | const index = x + y * size; 58 | const dx = size * 0.5 - x; 59 | const dy = size * 0.5 - y; 60 | const peakDistance = Math.min(1, Math.sqrt(dx * dx + dy * dy) / size * 2); 61 | const multiplier = Heightmap.MULTIPLIER * Math.pow(0.5 + 0.5 * Math.cos(Math.PI * peakDistance), peakPower); 62 | 63 | if (multiplier === 0) { 64 | types[index] = Heightmap.TYPE_DEFAULT; 65 | heights[index] = 0; 66 | 67 | continue; 68 | } 69 | 70 | let sample = 0; 71 | let influence = Heightmap.OCTAVE_INFLUENCE_INITIAL; 72 | let scale = maxScale; 73 | 74 | for (let octave = 0; octave < Heightmap.OCTAVES; ++octave) { 75 | sample += noises[octave].sample(x * scale, y * scale) * influence; 76 | 77 | influence /= Heightmap.OCTAVE_FALLOFF; 78 | scale *= Heightmap.SCALE_FALLOFF; 79 | } 80 | 81 | let height = multiplier * Math.pow(sample, power) - waterThreshold; 82 | 83 | if (height > 1) { 84 | height = Math.max(Heightmap.VOLCANO_MIN, 1 - Math.min(1, Math.max(0, height - 1 - Heightmap.VOLCANO_RIM))); 85 | 86 | if (height === 1) 87 | types[index] = Heightmap.TYPE_DEFAULT; 88 | else 89 | types[index] = Heightmap.TYPE_VOLCANO; 90 | } 91 | else 92 | types[index] = Heightmap.TYPE_DEFAULT; 93 | 94 | heights[index] = Math.max(0, height); 95 | 96 | if (heights[index] !== 0) { 97 | if (x < xMin) 98 | xMin = x; 99 | 100 | if (x > xMax) 101 | xMax = x; 102 | 103 | if (y < yMin) 104 | yMin = y; 105 | 106 | if (y > yMax) 107 | yMax = y; 108 | 109 | if (heights[index] > zMax) 110 | zMax = heights[index]; 111 | } 112 | } 113 | 114 | calculateNormals(); 115 | }; 116 | 117 | this.getSize = () => size; 118 | this.getHeight = (x, y) => heights[x + size * y]; 119 | this.getNormal = (x, y) => normals[x + size * y]; 120 | this.getType = (x, y) => types[x + size * y]; 121 | this.getBounds = height => new Bounds( 122 | new Vector3(Math.floor(xMin), Math.floor(yMin), 0), 123 | new Vector3(Math.ceil(xMax), Math.ceil(yMax), Math.ceil(zMax * height))); 124 | 125 | fill(); 126 | }; 127 | 128 | Heightmap.TYPE_DEFAULT = 0; 129 | Heightmap.TYPE_VOLCANO = 1; 130 | Heightmap.VOLCANO_RIM = 0.07; 131 | Heightmap.NORMAL_EDGE = new Vector3(0, 0, -1); 132 | Heightmap.WATER_THRESHOLD_MIN = 0.06; 133 | Heightmap.WATER_THRESHOLD_MAX = 0.1; 134 | Heightmap.POWER_MIN = 3.2; 135 | Heightmap.POWER_MAX = 3.8; 136 | Heightmap.MULTIPLIER = 5; 137 | Heightmap.PEAK_POWER_MIN = 0.6; 138 | Heightmap.PEAK_POWER_MAX = 1; 139 | Heightmap.VOLCANO_MIN = 0.85; 140 | Heightmap.SCALE_MIN = 4; 141 | Heightmap.SCALE_MAX = 6; 142 | Heightmap.SCALE_FALLOFF = 1.8; 143 | Heightmap.OCTAVES = 7; 144 | Heightmap.OCTAVE_FALLOFF = 2.4; 145 | Heightmap.OCTAVE_INFLUENCE_INITIAL = ((Heightmap.OCTAVE_FALLOFF - 1) * 146 | (Math.pow(Heightmap.OCTAVE_FALLOFF, Heightmap.OCTAVES))) / 147 | (Math.pow(Heightmap.OCTAVE_FALLOFF, Heightmap.OCTAVES) - 1) / Heightmap.OCTAVE_FALLOFF; 148 | Heightmap.GRADIENT_BEACH_START = 0; 149 | Heightmap.GRADIENT_BEACH_END = 0.08; 150 | Heightmap.GRADIENT_GRASS_START = 0.25; 151 | Heightmap.GRADIENT_GRASS_END = 0.75; 152 | Heightmap.GRADIENT_MOUNTAIN_START = 0.85; 153 | Heightmap.GRADIENT_MOUNTAIN_END = 1; 154 | Heightmap.GRADIENT_VOLCANO_SURFACE = 0.9; 155 | Heightmap.GRADIENT_VOLCANO_DEEP = Heightmap.VOLCANO_MIN; 156 | Heightmap.GRADIENTS = [ 157 | new Gradient([ 158 | new Gradient.Stop(Heightmap.GRADIENT_BEACH_START, StyleUtils.getColor("--color-beach-start")), 159 | new Gradient.Stop(Heightmap.GRADIENT_BEACH_END, StyleUtils.getColor("--color-beach-end")), 160 | new Gradient.Stop(Heightmap.GRADIENT_GRASS_START, StyleUtils.getColor("--color-grass-start")), 161 | new Gradient.Stop(Heightmap.GRADIENT_GRASS_END, StyleUtils.getColor("--color-grass-end")), 162 | new Gradient.Stop(Heightmap.GRADIENT_MOUNTAIN_START, StyleUtils.getColor("--color-mountain-start")), 163 | new Gradient.Stop(Heightmap.GRADIENT_MOUNTAIN_END, StyleUtils.getColor("--color-mountain-end"))]), 164 | new Gradient([ 165 | new Gradient.Stop(0, StyleUtils.getColor("--color-volcano-deep")), 166 | new Gradient.Stop(Heightmap.GRADIENT_VOLCANO_DEEP, StyleUtils.getColor("--color-volcano-deep")), 167 | new Gradient.Stop(Heightmap.GRADIENT_VOLCANO_SURFACE, StyleUtils.getColor("--color-volcano-surface")), 168 | new Gradient.Stop(1, StyleUtils.getColor("--color-mountain-end"))])]; -------------------------------------------------------------------------------- /js/myr.js: -------------------------------------------------------------------------------- 1 | const Myr = function(canvasElement, antialias, alpha) { 2 | const _gl = canvasElement.getContext("webgl2", { 3 | antialias: antialias ? antialias : false, 4 | depth: false, 5 | alpha: alpha ? alpha : false 6 | }); 7 | 8 | const Renderable = {}; 9 | 10 | Renderable.prototype = { 11 | draw: function(x, y) { 12 | this._prepareDraw(); 13 | 14 | setAttributesUv(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight()); 15 | setAttributesDraw(x, y, this.getWidth(), this.getHeight()); 16 | }, 17 | 18 | drawScaled: function(x, y, xScale, yScale) { 19 | this._prepareDraw(); 20 | 21 | setAttributesUv(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight()); 22 | setAttributesDraw(x, y, this.getWidth() * xScale, this.getHeight() * yScale); 23 | }, 24 | 25 | drawSheared: function(x, y, xShear, yShear) { 26 | this._prepareDraw(); 27 | 28 | setAttributesUv(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight()); 29 | setAttributesDrawSheared(x, y, this.getWidth(), this.getHeight(), xShear, yShear); 30 | }, 31 | 32 | drawRotated: function(x, y, angle) { 33 | this._prepareDraw(); 34 | 35 | setAttributesUv(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight()); 36 | setAttributesDrawRotated(x, y, this.getWidth(), this.getHeight(), angle); 37 | }, 38 | 39 | drawScaledRotated: function(x, y, xScale, yScale, angle) { 40 | this._prepareDraw(); 41 | 42 | setAttributesUv(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight()); 43 | setAttributesDrawRotated(x, y, this.getWidth() * xScale, this.getHeight() * yScale, angle); 44 | }, 45 | 46 | drawTransformed: function(transform) { 47 | this._prepareDraw(); 48 | 49 | setAttributesUv(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight()); 50 | setAttributesDrawTransform(transform, this.getWidth(), this.getHeight()); 51 | }, 52 | 53 | drawTransformedAt: function(x, y, transform) { 54 | this._prepareDraw(); 55 | 56 | setAttributesUv(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight()); 57 | setAttributesDrawTransformAt(x, y, transform, this.getWidth(), this.getHeight()); 58 | }, 59 | 60 | drawPart: function(x, y, left, top, w, h) { 61 | this._prepareDraw(); 62 | 63 | const wf = 1 / this.getWidth(); 64 | const hf = 1 / this.getHeight(); 65 | 66 | setAttributesUvPart(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight(), left * wf, top * hf, w * wf, h * hf); 67 | setAttributesDraw(x, y, w, h); 68 | }, 69 | 70 | drawPartTransformed: function(transform, left, top, w, h) { 71 | this._prepareDraw(); 72 | 73 | const wf = 1 / this.getWidth(); 74 | const hf = 1 / this.getHeight(); 75 | 76 | setAttributesUvPart(this.getUvLeft(), this.getUvTop(), this.getUvWidth(), this.getUvHeight(), left * wf, top * hf, w * wf, h * hf); 77 | setAttributesDrawTransform(transform, w, h); 78 | } 79 | }; 80 | 81 | this.Surface = function() { 82 | this.free = () => { 83 | _gl.deleteTexture(_texture); 84 | _gl.deleteFramebuffer(_framebuffer); 85 | }; 86 | 87 | this.bind = () => { 88 | bind(this); 89 | 90 | _gl.bindFramebuffer(_gl.FRAMEBUFFER, _framebuffer); 91 | _gl.viewport(0, 0, _width, _height); 92 | }; 93 | 94 | this._prepareDraw = () => { 95 | bindTextureSurface(_texture); 96 | prepareDraw(RENDER_MODE_SURFACES, 12); 97 | 98 | _instanceBuffer[++_instanceBufferAt] = 0; 99 | _instanceBuffer[++_instanceBufferAt] = 0; 100 | }; 101 | 102 | this._addFrame = frame => { 103 | if(_ready) { 104 | frame[5] /= _width; 105 | frame[6] /= _height; 106 | frame[7] /= _width; 107 | frame[8] /= _height; 108 | } 109 | else 110 | _frames.push(frame); 111 | }; 112 | 113 | this._getTexture = () => _texture; 114 | this.getWidth = () => _width; 115 | this.getHeight = () => _height; 116 | this.setClearColor = color => _clearColor = color; 117 | this.clear = () => clear(_clearColor); 118 | this.ready = () => _ready; 119 | 120 | const _texture = _gl.createTexture(); 121 | const _framebuffer = _gl.createFramebuffer(); 122 | const _frames = []; 123 | 124 | let _ready = false; 125 | let _width = 0; 126 | let _height = 0; 127 | let _clearColor = new Myr.Color(1, 1, 1, 0); 128 | let _linear = false; 129 | let _repeat = false; 130 | 131 | _gl.activeTexture(TEXTURE_EDITING); 132 | _gl.bindTexture(_gl.TEXTURE_2D, _texture); 133 | 134 | if(typeof arguments[0] === "number") { 135 | _width = arguments[0]; 136 | _height = arguments[1]; 137 | _ready = true; 138 | 139 | if(arguments[2] !== undefined && typeof arguments[2] !== "number") 140 | _gl.texImage2D( 141 | _gl.TEXTURE_2D, 0, _gl.RGBA, _width, _height, 0, _gl.RGBA, _gl.UNSIGNED_BYTE, 142 | arguments[2]); 143 | else switch (arguments[2]) { 144 | default: 145 | case 0: 146 | const initial = new Uint8Array(_width * _height << 2); 147 | 148 | for (let i = 0; i < initial.length; i += 4) { 149 | initial[i] = initial[i + 1] = initial[i + 2] = 255; 150 | initial[i + 3] = 0; 151 | } 152 | 153 | _gl.texImage2D( 154 | _gl.TEXTURE_2D, 0, _gl.RGBA, _width, _height, 0, _gl.RGBA, _gl.UNSIGNED_BYTE, 155 | initial); 156 | 157 | break; 158 | case 1: 159 | _gl.texImage2D( 160 | _gl.TEXTURE_2D, 0, _gl.RGBA16F, _width, _height, 0, _gl.RGBA, _gl.FLOAT, 161 | new Float32Array(_width * _height << 2)); 162 | 163 | break; 164 | case 2: 165 | _gl.texImage2D( 166 | _gl.TEXTURE_2D, 0, _gl.RGBA32F, _width, _height, 0, _gl.RGBA, _gl.FLOAT, 167 | new Float32Array(_width * _height << 2)); 168 | 169 | break; 170 | } 171 | 172 | if (arguments[3] === true) 173 | _linear = true; 174 | 175 | if (arguments[4] === true) 176 | _repeat = true; 177 | } 178 | else { 179 | const image = new Image(); 180 | 181 | image.onload = () => { 182 | if(_width === 0 || _height === 0) { 183 | _width = image.width; 184 | _height = image.height; 185 | } 186 | 187 | _gl.activeTexture(TEXTURE_EDITING); 188 | _gl.bindTexture(_gl.TEXTURE_2D, _texture); 189 | _gl.texImage2D(_gl.TEXTURE_2D, 0, _gl.RGBA, _gl.RGBA, _gl.UNSIGNED_BYTE, image); 190 | 191 | for(let frame = _frames.pop(); frame !== undefined; frame = _frames.pop()) { 192 | frame[5] /= _width; 193 | frame[6] /= _height; 194 | frame[7] /= _width; 195 | frame[8] /= _height; 196 | } 197 | 198 | _ready = true; 199 | }; 200 | 201 | const source = arguments[0]; 202 | 203 | if (source instanceof Image) { 204 | image.crossOrigin = source.crossOrigin; 205 | image.src = source.src; 206 | image.width = source.width; 207 | image.height = source.height; 208 | } else { 209 | image.crossOrigin = "Anonymous"; 210 | image.src = source; 211 | } 212 | 213 | if (arguments[2] !== undefined && (typeof arguments[2]) === "number") { 214 | _width = arguments[1]; 215 | _height = arguments[2]; 216 | 217 | if (arguments[3] === true) 218 | _linear = true; 219 | 220 | if (arguments[4] === true) 221 | _repeat = true; 222 | } 223 | else { 224 | if (arguments[1] === true) 225 | _linear = true; 226 | 227 | if (arguments[2] === true) 228 | _repeat = true; 229 | } 230 | 231 | _gl.texImage2D(_gl.TEXTURE_2D, 0, _gl.RGBA, 1, 1, 0, _gl.RGBA, _gl.UNSIGNED_BYTE, _emptyPixel); 232 | } 233 | 234 | if (_linear) { 235 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MAG_FILTER, _gl.LINEAR); 236 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, _gl.LINEAR); 237 | } 238 | else { 239 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MAG_FILTER, _gl.NEAREST); 240 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_MIN_FILTER, _gl.NEAREST); 241 | } 242 | 243 | if (_repeat) { 244 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_S, _gl.REPEAT); 245 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_T, _gl.REPEAT); 246 | } 247 | else { 248 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_S, _gl.CLAMP_TO_EDGE); 249 | _gl.texParameteri(_gl.TEXTURE_2D, _gl.TEXTURE_WRAP_T, _gl.CLAMP_TO_EDGE); 250 | } 251 | 252 | { 253 | const previousFramebuffer = _gl.getParameter(_gl.FRAMEBUFFER_BINDING); 254 | 255 | _gl.bindFramebuffer(_gl.FRAMEBUFFER, _framebuffer); 256 | _gl.framebufferTexture2D(_gl.FRAMEBUFFER, _gl.COLOR_ATTACHMENT0, _gl.TEXTURE_2D, _texture, 0); 257 | _gl.bindFramebuffer(_gl.FRAMEBUFFER, previousFramebuffer); 258 | } 259 | }; 260 | 261 | this.Surface.prototype = Object.create(Renderable.prototype); 262 | this.Surface.prototype.getUvLeft = () => 0; 263 | this.Surface.prototype.getUvTop = () => 0; 264 | this.Surface.prototype.getUvWidth = () => 1; 265 | this.Surface.prototype.getUvHeight = () => 1; 266 | 267 | this.Sprite = function(name) { 268 | this.animate = timeStep => { 269 | if (this.isFinished()) 270 | return; 271 | 272 | _frameCounter += timeStep; 273 | 274 | while (_frameCounter > this._getFrame()[9]) { 275 | _frameCounter -= this._getFrame()[9]; 276 | 277 | if (++_frame === _frames.length) 278 | _frame = 0; 279 | 280 | if (this.isFinished()) 281 | break; 282 | } 283 | }; 284 | 285 | this._setMeshBounds = () => { 286 | _meshUvLeft = this._getFrame()[5]; 287 | _meshUvTop = this._getFrame()[6]; 288 | _meshUvWidth = this._getFrame()[7]; 289 | _meshUvHeight = this._getFrame()[8]; 290 | }; 291 | 292 | this._prepareDraw = () => { 293 | const frame = this._getFrame(); 294 | 295 | bindTextureAtlas(frame[0]); 296 | prepareDraw(RENDER_MODE_SPRITES, 12); 297 | 298 | _instanceBuffer[++_instanceBufferAt] = frame[3]; 299 | _instanceBuffer[++_instanceBufferAt] = frame[4]; 300 | }; 301 | 302 | this._getFrame = () => _frames[_frame]; 303 | this.setFrame = index => _frame = index; 304 | this.getFrame = () => _frame; 305 | this.getFrameCount = () => _frames.length; 306 | 307 | const _frames = _sprites[name]; 308 | let _frameCounter = 0; 309 | let _frame = 0; 310 | }; 311 | 312 | this.Sprite.prototype = Object.create(Renderable.prototype); 313 | 314 | this.Sprite.prototype._getTexture = function() { 315 | return this._getFrame()[0]; 316 | }; 317 | 318 | this.Sprite.prototype.getUvLeft = function() { 319 | return this._getFrame()[5]; 320 | }; 321 | 322 | this.Sprite.prototype.getUvTop = function() { 323 | return this._getFrame()[6]; 324 | }; 325 | 326 | this.Sprite.prototype.getUvWidth = function() { 327 | return this._getFrame()[7]; 328 | }; 329 | 330 | this.Sprite.prototype.getUvHeight = function() { 331 | return this._getFrame()[8]; 332 | }; 333 | 334 | this.Sprite.prototype.isFinished = function() { 335 | return this._getFrame()[9] < 0; 336 | }; 337 | 338 | this.Sprite.prototype.getWidth = function() { 339 | return this._getFrame()[1]; 340 | }; 341 | 342 | this.Sprite.prototype.getHeight = function() { 343 | return this._getFrame()[2]; 344 | }; 345 | 346 | this.Sprite.prototype.getOriginX = function() { 347 | return this._getFrame()[3] * this.getWidth(); 348 | }; 349 | 350 | this.Sprite.prototype.getOriginY = function() { 351 | return this._getFrame()[4] * this.getHeight(); 352 | }; 353 | 354 | this.Sprite.prototype.finished = function() { 355 | return this._getFrame()[9] < 0; 356 | }; 357 | 358 | const _shaderVariables = [ 359 | { 360 | name: "pixelSize", 361 | storage: "flat", 362 | type: "mediump vec2", 363 | value: "1.0/vec2(a2.z,a3.y)" 364 | }, 365 | { 366 | name: "pixel", 367 | storage: "", 368 | type: "mediump vec2", 369 | value: "vec2(a2.z, a3.y)*vertex" 370 | } 371 | ]; 372 | 373 | this.Shader = function(fragment, surfaces, variables) { 374 | const makeUniformsObject = () => { 375 | const uniforms = {}; 376 | 377 | for (let i = 0; i < surfaces.length; ++i) 378 | uniforms[surfaces[i]] = { 379 | type: "1i", 380 | value: 4 + i 381 | }; 382 | 383 | for (const variable of variables) 384 | uniforms[variable] = { 385 | type: "1f", 386 | value: 0 387 | }; 388 | 389 | return uniforms; 390 | }; 391 | 392 | const makeUniformsDeclaration = () => { 393 | let result = ""; 394 | 395 | for (const surface of surfaces) 396 | result += "uniform sampler2D " + surface + ";"; 397 | 398 | for (const variable of variables) 399 | result += "uniform mediump float " + variable + ";"; 400 | 401 | return result; 402 | }; 403 | 404 | const makeVariablesOut = () => { 405 | let result = ""; 406 | 407 | for (const variable of _shaderVariables) if (fragment.includes(variable.name)) 408 | result += variable.storage + " out " + variable.type + " " + variable.name + ";"; 409 | 410 | return result; 411 | }; 412 | 413 | const makeVariablesOutAssignments = () => { 414 | let result = ""; 415 | 416 | for (const variable of _shaderVariables) if (fragment.includes(variable.name)) 417 | result += variable.name + "=" + variable.value + ";"; 418 | 419 | return result; 420 | }; 421 | 422 | const makeVariablesIn = () => { 423 | let result = ""; 424 | 425 | for (const variable of _shaderVariables) if (fragment.includes(variable.name)) 426 | result += variable.storage + " in " + variable.type + " " + variable.name + ";"; 427 | 428 | return result; 429 | }; 430 | 431 | const bindTextures = () => { 432 | for (let i = 0; i < surfaces.length; ++i) { 433 | _gl.activeTexture(TEXTURE_SHADER_FIRST + i); 434 | _gl.bindTexture(_gl.TEXTURE_2D, _surfaceTextures[i]); 435 | } 436 | }; 437 | 438 | const _core = new ShaderCore( 439 | "layout(location=0) in mediump vec2 vertex;" + 440 | "layout(location=1) in mediump vec4 a1;" + 441 | "layout(location=2) in mediump vec4 a2;" + 442 | "layout(location=3) in mediump vec4 a3;" + 443 | _uniformBlock + 444 | "out mediump vec2 uv;" + 445 | makeVariablesOut() + 446 | "void main() {" + 447 | "uv=a1.zw+vertex*a2.xy;" + 448 | "mediump vec2 transformed=(((vertex-a1.xy)*" + 449 | "mat2(a2.zw,a3.xy)+a3.zw)*" + 450 | "mat2(tw.xy,th.xy)+vec2(tw.z,th.z))/" + 451 | "vec2(tw.w,th.w)*2.0;" + 452 | makeVariablesOutAssignments() + 453 | "gl_Position=vec4(transformed-vec2(1),0,1);" + 454 | "}", 455 | makeUniformsDeclaration() + 456 | _uniformBlock + 457 | "in mediump vec2 uv;" + 458 | makeVariablesIn() + 459 | "layout(location=0) out lowp vec4 color;" + 460 | fragment 461 | ); 462 | 463 | const _shader = new Shader(_core, makeUniformsObject()); 464 | const _surfaceTextures = new Array(surfaces.length); 465 | let _width = -1; 466 | let _height = 0; 467 | 468 | this.free = () => _shader.free(); 469 | this.getWidth = () => _width; 470 | this.getHeight = () => _height; 471 | this.setVariable = (name, value) => _shader.setUniform(name, value); 472 | this.setSurface = (name, surface) => { 473 | const index = surfaces.indexOf(name); 474 | 475 | if (_width === -1 && index === 0) { 476 | _width = surface.getWidth(); 477 | _height = surface.getHeight(); 478 | } 479 | 480 | _surfaceTextures[index] = surface._getTexture(); 481 | }; 482 | 483 | this.setSize = (width, height) => { 484 | _width = width; 485 | _height = height; 486 | }; 487 | 488 | this._prepareDraw = () => { 489 | prepareDraw(RENDER_MODE_SHADER, 12, _shader); 490 | bindTextures(); 491 | 492 | _instanceBuffer[++_instanceBufferAt] = 0; 493 | _instanceBuffer[++_instanceBufferAt] = 0; 494 | }; 495 | }; 496 | 497 | this.Shader.prototype = Object.create(Renderable.prototype); 498 | this.Shader.prototype.getUvLeft = () => 0; 499 | this.Shader.prototype.getUvTop = () => 0; 500 | this.Shader.prototype.getUvWidth = () => 1; 501 | this.Shader.prototype.getUvHeight = () => 1; 502 | 503 | const setAttributesUv = (uvLeft, uvTop, uvWidth, uvHeight) => { 504 | _instanceBuffer[++_instanceBufferAt] = uvLeft; 505 | _instanceBuffer[++_instanceBufferAt] = uvTop; 506 | _instanceBuffer[++_instanceBufferAt] = uvWidth; 507 | _instanceBuffer[++_instanceBufferAt] = uvHeight; 508 | }; 509 | 510 | const setAttributesUvPart = (uvLeft, uvTop, uvWidth, uvHeight, left, top, width, height) => { 511 | _instanceBuffer[++_instanceBufferAt] = uvLeft + uvWidth * left; 512 | _instanceBuffer[++_instanceBufferAt] = uvTop + uvHeight * top; 513 | _instanceBuffer[++_instanceBufferAt] = uvWidth * width; 514 | _instanceBuffer[++_instanceBufferAt] = uvHeight * height; 515 | }; 516 | 517 | const setAttributesDraw = (x, y, width, height) => { 518 | _instanceBuffer[++_instanceBufferAt] = width; 519 | _instanceBuffer[++_instanceBufferAt] = _instanceBuffer[++_instanceBufferAt] = 0; 520 | _instanceBuffer[++_instanceBufferAt] = height; 521 | _instanceBuffer[++_instanceBufferAt] = x; 522 | _instanceBuffer[++_instanceBufferAt] = y; 523 | }; 524 | 525 | const setAttributesDrawSheared = (x, y, width, height, xShear, yShear) => { 526 | _instanceBuffer[++_instanceBufferAt] = width; 527 | _instanceBuffer[++_instanceBufferAt] = width * xShear; 528 | _instanceBuffer[++_instanceBufferAt] = height * yShear; 529 | _instanceBuffer[++_instanceBufferAt] = height; 530 | _instanceBuffer[++_instanceBufferAt] = x; 531 | _instanceBuffer[++_instanceBufferAt] = y; 532 | }; 533 | 534 | const setAttributesDrawRotated = (x, y, width, height, angle) => { 535 | const cos = Math.cos(angle); 536 | const sin = Math.sin(angle); 537 | 538 | _instanceBuffer[++_instanceBufferAt] = cos * width; 539 | _instanceBuffer[++_instanceBufferAt] = sin * height; 540 | _instanceBuffer[++_instanceBufferAt] = -sin * width; 541 | _instanceBuffer[++_instanceBufferAt] = cos * height; 542 | _instanceBuffer[++_instanceBufferAt] = x; 543 | _instanceBuffer[++_instanceBufferAt] = y; 544 | }; 545 | 546 | const setAttributesDrawTransform = (transform, width, height) => { 547 | _instanceBuffer[++_instanceBufferAt] = transform._00 * width; 548 | _instanceBuffer[++_instanceBufferAt] = transform._10 * height; 549 | _instanceBuffer[++_instanceBufferAt] = transform._01 * width; 550 | _instanceBuffer[++_instanceBufferAt] = transform._11 * height; 551 | _instanceBuffer[++_instanceBufferAt] = transform._20; 552 | _instanceBuffer[++_instanceBufferAt] = transform._21; 553 | }; 554 | 555 | const setAttributesDrawTransformAt = (x, y, transform, width, height) => { 556 | _instanceBuffer[++_instanceBufferAt] = transform._00 * width; 557 | _instanceBuffer[++_instanceBufferAt] = transform._10 * height; 558 | _instanceBuffer[++_instanceBufferAt] = transform._01 * width; 559 | _instanceBuffer[++_instanceBufferAt] = transform._11 * height; 560 | _instanceBuffer[++_instanceBufferAt] = transform._20 + x; 561 | _instanceBuffer[++_instanceBufferAt] = transform._21 + y; 562 | }; 563 | 564 | const pushVertexColor = (mode, color, x, y) => { 565 | prepareDraw(mode, 6); 566 | 567 | _instanceBuffer[++_instanceBufferAt] = color.r; 568 | _instanceBuffer[++_instanceBufferAt] = color.g; 569 | _instanceBuffer[++_instanceBufferAt] = color.b; 570 | _instanceBuffer[++_instanceBufferAt] = color.a; 571 | _instanceBuffer[++_instanceBufferAt] = x; 572 | _instanceBuffer[++_instanceBufferAt] = y; 573 | }; 574 | 575 | const _primitivesCirclePoints = new Array(1024); 576 | const _primitivesGetCircleStep = radius => Math.max(2, 32 >> Math.floor(radius / 128)); 577 | 578 | for(let i = 0; i < 1024; i += 2) { 579 | const radians = i * Math.PI / 512; 580 | 581 | _primitivesCirclePoints[i] = Math.cos(radians); 582 | _primitivesCirclePoints[i + 1] = Math.sin(radians); 583 | } 584 | 585 | this.primitives = {}; 586 | 587 | this.primitives.drawPoint = (color, x, y) => { 588 | pushVertexColor(RENDER_MODE_POINTS, color, x + 1, y + 1); 589 | }; 590 | 591 | this.primitives.drawLine = (color, x1, y1, x2, y2) => { 592 | pushVertexColor(RENDER_MODE_LINES, color, x1, y1); 593 | pushVertexColor(RENDER_MODE_LINES, color, x2, y2); 594 | }; 595 | 596 | this.primitives.drawLineGradient = (color1, x1, y1, color2, x2, y2) => { 597 | pushVertexColor(RENDER_MODE_LINES, color1, x1, y1); 598 | pushVertexColor(RENDER_MODE_LINES, color2, x2, y2); 599 | }; 600 | 601 | this.primitives.drawRectangle = (color, x, y, width, height) => { 602 | this.primitives.drawLine(color, x, y, x + width, y); 603 | this.primitives.drawLine(color, x + width, y, x + width, y + height); 604 | this.primitives.drawLine(color, x + width, y + height, x, y + height); 605 | this.primitives.drawLine(color, x, y + height, x, y); 606 | }; 607 | 608 | this.primitives.drawCircle = (color, x, y, radius) => { 609 | const step = _primitivesGetCircleStep(radius); 610 | let i; 611 | 612 | for(i = 0; i < 1024 - step; i += step) 613 | this.primitives.drawLine( 614 | color, 615 | x + _primitivesCirclePoints[i] * radius, 616 | y + _primitivesCirclePoints[i + 1] * radius, 617 | x + _primitivesCirclePoints[i + step] * radius, 618 | y + _primitivesCirclePoints[i + 1 + step] * radius); 619 | 620 | this.primitives.drawLine( 621 | color, 622 | x + _primitivesCirclePoints[i] * radius, 623 | y + _primitivesCirclePoints[i + 1] * radius, 624 | x + _primitivesCirclePoints[0] * radius, 625 | y + _primitivesCirclePoints[1] * radius); 626 | }; 627 | 628 | this.primitives.drawTriangle = (color, x1, y1, x2, y2, x3, y3) => { 629 | pushVertexColor(RENDER_MODE_TRIANGLES, color, x1, y1); 630 | pushVertexColor(RENDER_MODE_TRIANGLES, color, x2, y2); 631 | pushVertexColor(RENDER_MODE_TRIANGLES, color, x3, y3); 632 | }; 633 | 634 | this.primitives.drawTriangleGradient = (color1, x1, y1, color2, x2, y2, color3, x3, y3) => { 635 | pushVertexColor(RENDER_MODE_TRIANGLES, color1, x1, y1); 636 | pushVertexColor(RENDER_MODE_TRIANGLES, color2, x2, y2); 637 | pushVertexColor(RENDER_MODE_TRIANGLES, color3, x3, y3); 638 | }; 639 | 640 | this.primitives.fillRectangle = (color, x, y, width, height) => { 641 | this.primitives.drawTriangle(color, x, y, x, y + height, x + width, y + height); 642 | this.primitives.drawTriangle(color, x + width, y + height, x + width, y, x, y); 643 | }; 644 | 645 | this.primitives.fillRectangleGradient = (color1, color2, color3, color4, x, y, width, height) => { 646 | this.primitives.drawTriangleGradient(color1, x, y, color3, x, y + height, color4, x + width, y + height); 647 | this.primitives.drawTriangleGradient(color4, x + width, y + height, color2, x + width, y, color1, x, y); 648 | }; 649 | 650 | this.primitives.fillCircle = (color, x, y, radius) => { 651 | const step = _primitivesGetCircleStep(radius); 652 | let i = 0; 653 | 654 | for(; i < 1024 - step; i+= step) 655 | this.primitives.drawTriangle( 656 | color, 657 | x, y, 658 | x + _primitivesCirclePoints[i] * radius, 659 | y + _primitivesCirclePoints[i + 1] * radius, 660 | x + _primitivesCirclePoints[i + step] * radius, 661 | y + _primitivesCirclePoints[i + 1 + step] * radius); 662 | 663 | this.primitives.drawTriangle( 664 | color, 665 | x, y, 666 | x + _primitivesCirclePoints[i] * radius, 667 | y + _primitivesCirclePoints[i + 1] * radius, 668 | x + _primitivesCirclePoints[0] * radius, 669 | y + _primitivesCirclePoints[1] * radius); 670 | }; 671 | 672 | this.primitives.fillCircleGradient = (colorStart, colorEnd, x, y, radius) => { 673 | const step = _primitivesGetCircleStep(radius); 674 | let i; 675 | 676 | for(i = 0; i < 1024 - step; i+= step) 677 | this.primitives.drawTriangleGradient( 678 | colorStart, 679 | x, y, 680 | colorEnd, 681 | x + _primitivesCirclePoints[i] * radius, 682 | y + _primitivesCirclePoints[i + 1] * radius, 683 | colorEnd, 684 | x + _primitivesCirclePoints[i + step] * radius, 685 | y + _primitivesCirclePoints[i + 1 + step] * radius); 686 | 687 | this.primitives.drawTriangleGradient( 688 | colorStart, 689 | x, y, 690 | colorEnd, 691 | x + _primitivesCirclePoints[i] * radius, 692 | y + _primitivesCirclePoints[i + 1] * radius, 693 | colorEnd, 694 | x + _primitivesCirclePoints[0] * radius, 695 | y + _primitivesCirclePoints[1] * radius); 696 | }; 697 | 698 | const meshBindSource = source => { 699 | if(source instanceof this.Surface) { 700 | _meshUvLeft = _meshUvTop = 0; 701 | _meshUvWidth = _meshUvHeight = 1; 702 | } 703 | else 704 | source._setMeshBounds(); 705 | 706 | if(_currentTextureMesh === source._getTexture()) 707 | return; 708 | 709 | flush(); 710 | 711 | _gl.activeTexture(TEXTURE_MESH); 712 | _gl.bindTexture(_gl.TEXTURE_2D, source._getTexture()); 713 | 714 | _currentTextureMesh = source._getTexture(); 715 | }; 716 | 717 | const pushVertexMesh = (mode, x, y, u, v) => { 718 | prepareDraw(mode, 4); 719 | 720 | _instanceBuffer[++_instanceBufferAt] = x; 721 | _instanceBuffer[++_instanceBufferAt] = y; 722 | _instanceBuffer[++_instanceBufferAt] = u * _meshUvWidth + _meshUvLeft; 723 | _instanceBuffer[++_instanceBufferAt] = v * _meshUvHeight + _meshUvTop; 724 | }; 725 | 726 | this.mesh = {}; 727 | 728 | this.mesh.drawTriangle = (source, x1, y1, u1, v1, x2, y2, u2, v2, x3, y3, u3, v3) => { 729 | meshBindSource(source); 730 | 731 | pushVertexMesh(RENDER_MODE_MESH, x1, y1, u1, v1); 732 | pushVertexMesh(RENDER_MODE_MESH, x2, y2, u2, v2); 733 | pushVertexMesh(RENDER_MODE_MESH, x3, y3, u3, v3); 734 | }; 735 | 736 | this.utils = {}; 737 | 738 | this.utils.loop = update => { 739 | let lastDate = new Date(); 740 | const loopFunction = function(step) { 741 | const date = new Date(); 742 | 743 | if(update((date - lastDate) * 0.001)) 744 | requestAnimationFrame(loopFunction); 745 | 746 | lastDate = date; 747 | }; 748 | 749 | requestAnimationFrame(loopFunction); 750 | }; 751 | 752 | const ShaderCore = function(vertex, fragment) { 753 | const createShader = (type, source) => { 754 | const shader = _gl.createShader(type); 755 | 756 | _gl.shaderSource(shader, "#version 300 es\n" + source); 757 | _gl.compileShader(shader); 758 | 759 | if(!_gl.getShaderParameter(shader, _gl.COMPILE_STATUS)) 760 | console.log(_gl.getShaderInfoLog(shader)); 761 | 762 | return shader; 763 | }; 764 | 765 | this.bind = () => { 766 | if(_currentShaderCore === this) 767 | return; 768 | 769 | _currentShaderCore = this; 770 | 771 | _gl.useProgram(_program); 772 | }; 773 | 774 | this.getProgram = () => _program; 775 | this.free = () => _gl.deleteProgram(_program); 776 | this.getVertex = () => vertex; 777 | this.getFragment = () => fragment; 778 | 779 | const _program = _gl.createProgram(); 780 | const _shaderVertex = createShader(_gl.VERTEX_SHADER, vertex); 781 | const _shaderFragment = createShader(_gl.FRAGMENT_SHADER, fragment); 782 | 783 | _gl.attachShader(_program, _shaderVertex); 784 | _gl.attachShader(_program, _shaderFragment); 785 | _gl.linkProgram(_program); 786 | _gl.detachShader(_program, _shaderVertex); 787 | _gl.detachShader(_program, _shaderFragment); 788 | _gl.deleteShader(_shaderVertex); 789 | _gl.deleteShader(_shaderFragment); 790 | }; 791 | 792 | const Shader = function(core, uniforms) { 793 | this.bind = () => { 794 | if(_currentShader === this) { 795 | for (const uniformCall of _uniformCalls) 796 | uniformCall[0](uniformCall[1], uniformCall[2].value); 797 | 798 | return; 799 | } 800 | 801 | _currentShader = this; 802 | 803 | core.bind(); 804 | 805 | for (const uniformCall of _uniformCalls) 806 | uniformCall[0](uniformCall[1], uniformCall[2].value); 807 | }; 808 | 809 | this.setUniform = (name, value) => uniforms[name].value = value; 810 | this.free = () => core.free(); 811 | 812 | const _uniformCalls = []; 813 | 814 | for (const uniform of Object.keys(uniforms)) 815 | _uniformCalls.push([ 816 | _gl["uniform" + uniforms[uniform].type].bind(_gl), 817 | _gl.getUniformLocation(core.getProgram(), uniform), 818 | uniforms[uniform] 819 | ]); 820 | }; 821 | 822 | const bind = target => { 823 | if(_surface === target) 824 | return; 825 | 826 | flush(); 827 | 828 | if(_surface != null) 829 | this.pop(); 830 | 831 | if(target != null) 832 | pushIdentity(); 833 | 834 | _surface = target; 835 | }; 836 | 837 | const bindTextureSurface = texture => { 838 | if(_currentTextureSurface === texture) 839 | return; 840 | 841 | flush(); 842 | 843 | _gl.activeTexture(TEXTURE_SURFACE); 844 | _gl.bindTexture(_gl.TEXTURE_2D, texture); 845 | 846 | _currentTextureSurface = texture; 847 | }; 848 | 849 | const bindTextureAtlas = texture => { 850 | if(_currentTextureAtlas === texture) 851 | return; 852 | 853 | flush(); 854 | 855 | _gl.activeTexture(TEXTURE_ATLAS); 856 | _gl.bindTexture(_gl.TEXTURE_2D, texture); 857 | 858 | _currentTextureAtlas = texture; 859 | }; 860 | 861 | const clear = color => { 862 | flush(); 863 | 864 | _gl.clearColor(color.r * _uboContents[8], color.g * _uboContents[9], color.b * _uboContents[10], color.a * _uboContents[11]); 865 | _gl.clear(_gl.COLOR_BUFFER_BIT); 866 | }; 867 | 868 | const flush = this.flush = () => { 869 | if(_instanceCount === 0) 870 | return; 871 | 872 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _instances); 873 | _gl.bufferSubData(_gl.ARRAY_BUFFER, 0, _instanceBuffer, 0, _instanceBufferAt + 1); 874 | 875 | switch(_renderMode) { 876 | case RENDER_MODE_SURFACES: 877 | case RENDER_MODE_SPRITES: 878 | case RENDER_MODE_SHADER: 879 | _gl.bindVertexArray(_vaoSprites); 880 | _gl.drawArraysInstanced(_gl.TRIANGLE_FAN, 0, 4, _instanceCount); 881 | break; 882 | case RENDER_MODE_LINES: 883 | _gl.bindVertexArray(_vaoLines); 884 | _gl.drawArrays(_gl.LINES, 0, _instanceCount); 885 | break; 886 | case RENDER_MODE_POINTS: 887 | _gl.bindVertexArray(_vaoLines); 888 | _gl.drawArrays(_gl.POINTS, 0, _instanceCount); 889 | break; 890 | case RENDER_MODE_TRIANGLES: 891 | _gl.bindVertexArray(_vaoLines); 892 | _gl.drawArrays(_gl.TRIANGLES, 0, _instanceCount); 893 | break; 894 | case RENDER_MODE_MESH: 895 | _gl.bindVertexArray(_vaoMesh); 896 | _gl.drawArrays(_gl.TRIANGLES, 0, _instanceCount); 897 | break; 898 | } 899 | 900 | _instanceBufferAt = -1; 901 | _instanceCount = 0; 902 | }; 903 | 904 | const sendUniformBuffer = () => { 905 | if(_surface == null) { 906 | _uboContents[3] = canvasElement.width; 907 | _uboContents[7] = canvasElement.height; 908 | } 909 | else { 910 | _uboContents[3] = _surface.getWidth(); 911 | _uboContents[7] = _surface.getHeight(); 912 | } 913 | 914 | _uboContents[0] = _transformStack[_transformAt]._00; 915 | _uboContents[1] = _transformStack[_transformAt]._10; 916 | _uboContents[2] = _transformStack[_transformAt]._20; 917 | _uboContents[4] = _transformStack[_transformAt]._01; 918 | _uboContents[5] = _transformStack[_transformAt]._11; 919 | _uboContents[6] = _transformStack[_transformAt]._21; 920 | 921 | _gl.bufferSubData(_gl.UNIFORM_BUFFER, 0, _uboContents); 922 | 923 | _transformDirty = false; 924 | }; 925 | 926 | const prepareDraw = (mode, size, shader) => { 927 | if(_transformDirty) { 928 | flush(); 929 | 930 | sendUniformBuffer(); 931 | } 932 | 933 | if(_renderMode !== mode || _renderMode === RENDER_MODE_SHADER) { 934 | flush(); 935 | 936 | _renderMode = mode; 937 | (shader || _shaders[mode]).bind(); 938 | } 939 | 940 | if(_instanceBufferAt + size >= _instanceBufferCapacity) { 941 | const oldBuffer = _instanceBuffer; 942 | 943 | _instanceBuffer = new Float32Array(_instanceBufferCapacity *= 2); 944 | 945 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _instances); 946 | _gl.bufferData(_gl.ARRAY_BUFFER, _instanceBufferCapacity * 4, _gl.DYNAMIC_DRAW); 947 | 948 | for(let i = 0; i < oldBuffer.byteLength; ++i) 949 | _instanceBuffer[i] = oldBuffer[i]; 950 | } 951 | 952 | ++_instanceCount; 953 | }; 954 | 955 | const pushIdentity = () => { 956 | if(++_transformAt === _transformStack.length) 957 | _transformStack.push(new Myr.Transform()); 958 | else 959 | _transformStack[_transformAt].identity(); 960 | 961 | _transformDirty = true; 962 | }; 963 | 964 | this.push = () => { 965 | if(++_transformAt === _transformStack.length) 966 | _transformStack.push(_transformStack[_transformAt - 1].copy()); 967 | else 968 | _transformStack[_transformAt].set(_transformStack[_transformAt - 1]); 969 | }; 970 | 971 | this.pop = () => { 972 | --_transformAt; 973 | 974 | _transformDirty = true; 975 | }; 976 | 977 | this.bind = () => { 978 | bind(null); 979 | 980 | _gl.bindFramebuffer(_gl.FRAMEBUFFER, null); 981 | _gl.viewport(0, 0, canvasElement.width, canvasElement.height); 982 | }; 983 | 984 | this.register = function() { 985 | const frames = []; 986 | 987 | for(let i = 1; i < arguments.length; ++i) 988 | frames.push(arguments[i]); 989 | 990 | if(_sprites[arguments[0]] === undefined) 991 | _sprites[arguments[0]] = frames; 992 | else { 993 | _sprites[arguments[0]].length = 0; 994 | 995 | for(let i = 0; i < frames.length; ++i) 996 | _sprites[arguments[0]].push(frames[i]); 997 | } 998 | }; 999 | 1000 | this.isRegistered = name => _sprites[name] !== undefined; 1001 | 1002 | this.makeSpriteFrame = (sheet, x, y, width, height, xOrigin, yOrigin, time) => { 1003 | const frame = [ 1004 | sheet._getTexture(), 1005 | width, 1006 | height, 1007 | xOrigin / width, 1008 | yOrigin / height, 1009 | x, 1010 | y, 1011 | width, 1012 | height, 1013 | time 1014 | ]; 1015 | 1016 | sheet._addFrame(frame); 1017 | 1018 | return frame; 1019 | }; 1020 | 1021 | this.free = () => { 1022 | for(let i = 0; i < _shaders.length; ++i) 1023 | _shaders[i].free(); 1024 | 1025 | _gl.deleteVertexArray(_vaoSprites); 1026 | _gl.deleteVertexArray(_vaoLines); 1027 | _gl.deleteVertexArray(_vaoMesh); 1028 | _gl.deleteBuffer(_quad); 1029 | _gl.deleteBuffer(_instances); 1030 | _gl.deleteBuffer(_ubo); 1031 | }; 1032 | 1033 | this.setColor = color => { 1034 | if( 1035 | _uboContents[8] === color.r && 1036 | _uboContents[9] === color.g && 1037 | _uboContents[10] === color.b && 1038 | _uboContents[11] === color.a) 1039 | return; 1040 | 1041 | flush(); 1042 | 1043 | _uboContents[8] = color.r; 1044 | _uboContents[9] = color.g; 1045 | _uboContents[10] = color.b; 1046 | _uboContents[11] = color.a; 1047 | 1048 | _gl.bufferSubData(_gl.UNIFORM_BUFFER, 0, _uboContents); 1049 | }; 1050 | 1051 | this.resize = (width, height) => { 1052 | canvasElement.width = width; 1053 | canvasElement.height = height; 1054 | 1055 | _transformStack[0]._21 = height; 1056 | 1057 | sendUniformBuffer(); 1058 | }; 1059 | 1060 | this.setAlpha = alpha => { 1061 | if(_uboContents[11] === alpha) 1062 | return; 1063 | 1064 | flush(); 1065 | 1066 | _uboContents[11] = alpha; 1067 | 1068 | _gl.bufferSubData(_gl.UNIFORM_BUFFER, 0, _uboContents); 1069 | }; 1070 | 1071 | this.blendEnable = () => { 1072 | flush(); 1073 | _gl.enable(_gl.BLEND); 1074 | }; 1075 | 1076 | this.blendDisable = () => { 1077 | flush(); 1078 | _gl.disable(_gl.BLEND); 1079 | }; 1080 | 1081 | const touchTransform = () => { 1082 | _transformDirty = true; 1083 | 1084 | return _transformStack[_transformAt]; 1085 | }; 1086 | 1087 | this.getTransform = () => _transformStack[_transformAt]; 1088 | this.transformSet = transform => { 1089 | touchTransform().set(_transformStack[0]); 1090 | touchTransform().multiply(transform); 1091 | } 1092 | this.transform = transform => touchTransform().multiply(transform); 1093 | this.translate = (x, y) => touchTransform().translate(x, y); 1094 | this.rotate = angle => touchTransform().rotate(angle); 1095 | this.shear = (x, y) => touchTransform().shear(x, y); 1096 | this.scale = (x, y) => touchTransform().scale(x, y); 1097 | this.setClearColor = color => _clearColor = color; 1098 | this.clear = () => clear(_clearColor); 1099 | this.getWidth = () => canvasElement.width; 1100 | this.getHeight = () => canvasElement.height; 1101 | this.unregister = name => delete _sprites[name]; 1102 | 1103 | const RENDER_MODE_NONE = -1; 1104 | const RENDER_MODE_SURFACES = 0; 1105 | const RENDER_MODE_SPRITES = 1; 1106 | const RENDER_MODE_LINES = 2; 1107 | const RENDER_MODE_POINTS = 3; 1108 | const RENDER_MODE_TRIANGLES = 4; 1109 | const RENDER_MODE_MESH = 5; 1110 | const RENDER_MODE_SHADER = 6; 1111 | const TEXTURE_ATLAS = _gl.TEXTURE0; 1112 | const TEXTURE_SURFACE = _gl.TEXTURE1; 1113 | const TEXTURE_MESH = _gl.TEXTURE2; 1114 | const TEXTURE_EDITING = _gl.TEXTURE3; 1115 | const TEXTURE_SHADER_FIRST = _gl.TEXTURE4; 1116 | 1117 | const _quad = _gl.createBuffer(); 1118 | const _instances = _gl.createBuffer(); 1119 | const _vaoSprites = _gl.createVertexArray(); 1120 | const _vaoLines = _gl.createVertexArray(); 1121 | const _vaoMesh = _gl.createVertexArray(); 1122 | const _ubo = _gl.createBuffer(); 1123 | const _uboContents = new Float32Array(12); 1124 | const _emptyPixel = new Uint8Array(4); 1125 | const _sprites = []; 1126 | const _transformStack = [new Myr.Transform(1, 0, 0, 0, -1, canvasElement.height)]; 1127 | const _uniformBlock = "layout(std140) uniform transform {mediump vec4 tw;mediump vec4 th;lowp vec4 colorFilter;};"; 1128 | const _shaderCoreSprites = new ShaderCore( 1129 | "layout(location=0) in mediump vec2 vertex;" + 1130 | "layout(location=1) in mediump vec4 a1;" + 1131 | "layout(location=2) in mediump vec4 a2;" + 1132 | "layout(location=3) in mediump vec4 a3;" + 1133 | _uniformBlock + 1134 | "out mediump vec2 uv;" + 1135 | "void main() {" + 1136 | "uv=a1.zw+vertex*a2.xy;" + 1137 | "mediump vec2 transformed=(((vertex-a1.xy)*" + 1138 | "mat2(a2.zw,a3.xy)+a3.zw)*" + 1139 | "mat2(tw.xy,th.xy)+vec2(tw.z,th.z))/" + 1140 | "vec2(tw.w,th.w)*2.0;" + 1141 | "gl_Position=vec4(transformed-vec2(1),0,1);" + 1142 | "}", 1143 | "uniform sampler2D source;" + 1144 | _uniformBlock + 1145 | "in mediump vec2 uv;" + 1146 | "layout(location=0) out lowp vec4 color;" + 1147 | "void main() {" + 1148 | "color=texture(source,uv)*colorFilter;" + 1149 | "}" 1150 | ); 1151 | const _shaderCoreLines = new ShaderCore( 1152 | "layout(location=0) in mediump vec4 color;" + 1153 | "layout(location=1) in mediump vec2 vertex;" + 1154 | _uniformBlock + 1155 | "out lowp vec4 colori;" + 1156 | "void main() {" + 1157 | "mediump vec2 transformed=(vertex*" + 1158 | "mat2(tw.xy,th.xy)+vec2(tw.z,th.z))/" + 1159 | "vec2(tw.w,th.w)*2.0;" + 1160 | "gl_Position=vec4(transformed-vec2(1),0,1);" + 1161 | "colori = color*colorFilter;" + 1162 | "}", 1163 | "in lowp vec4 colori;" + 1164 | "layout(location=0) out lowp vec4 color;" + 1165 | "void main() {" + 1166 | "color=colori;" + 1167 | "}" 1168 | ); 1169 | const _shaderCorePoints = new ShaderCore( 1170 | "layout(location=0) in mediump vec4 color;" + 1171 | "layout(location=1) in mediump vec2 vertex;" + 1172 | _uniformBlock + 1173 | "flat out lowp vec4 colorf;" + 1174 | "void main() {" + 1175 | "mediump vec2 transformed=(vertex*" + 1176 | "mat2(tw.xy,th.xy)+vec2(tw.z,th.z))/" + 1177 | "vec2(tw.w,th.w)*2.0;" + 1178 | "gl_Position=vec4(transformed-vec2(1),0,1);" + 1179 | "gl_PointSize=1.0;" + 1180 | "colorf = color*colorFilter;" + 1181 | "}", 1182 | "flat in lowp vec4 colorf;" + 1183 | "layout(location=0) out lowp vec4 color;" + 1184 | "void main() {" + 1185 | "color=colorf;" + 1186 | "}" 1187 | ); 1188 | const _shaderCoreMesh = new ShaderCore( 1189 | "layout(location=0) in mediump vec4 vertex;" + 1190 | _uniformBlock + 1191 | "out mediump vec2 uv;" + 1192 | "void main() {" + 1193 | "mediump vec2 transformed=(vertex.xy*" + 1194 | "mat2(tw.xy,th.xy)+vec2(tw.z,th.z))/" + 1195 | "vec2(tw.w,th.w)*2.0;" + 1196 | "gl_Position=vec4(transformed-vec2(1),0,1);" + 1197 | "uv = vertex.zw;" + 1198 | "}", 1199 | _shaderCoreSprites.getFragment() 1200 | ); 1201 | const _shaders = [ 1202 | new Shader( 1203 | _shaderCoreSprites, 1204 | { 1205 | source: { 1206 | type: "1i", 1207 | value: 1 1208 | } 1209 | }), 1210 | new Shader( 1211 | _shaderCoreSprites, 1212 | { 1213 | source: { 1214 | type: "1i", 1215 | value: 0 1216 | } 1217 | }), 1218 | new Shader( 1219 | _shaderCoreLines, 1220 | {}), 1221 | new Shader( 1222 | _shaderCorePoints, 1223 | {}), 1224 | new Shader( 1225 | _shaderCoreLines, 1226 | {}), 1227 | new Shader( 1228 | _shaderCoreMesh, 1229 | { 1230 | source: { 1231 | type: "1i", 1232 | value: 2 1233 | } 1234 | }) 1235 | ]; 1236 | 1237 | let _currentShader, _currentShaderCore, _surface, _currentTextureSurface, _currentTextureAtlas, _currentTextureMesh; 1238 | let _meshUvLeft, _meshUvTop, _meshUvWidth, _meshUvHeight; 1239 | let _transformAt = 0; 1240 | let _transformDirty = true; 1241 | let _renderMode = RENDER_MODE_NONE; 1242 | let _instanceBufferCapacity = 1024; 1243 | let _instanceBufferAt = -1; 1244 | let _instanceBuffer = new Float32Array(_instanceBufferCapacity); 1245 | let _instanceCount = 0; 1246 | let _clearColor = new Myr.Color(1, 1, 1, 0); 1247 | 1248 | _uboContents[8] = _uboContents[9] = _uboContents[10] = _uboContents[11] = 1; 1249 | 1250 | _gl.enable(_gl.BLEND); 1251 | _gl.disable(_gl.DEPTH_TEST); 1252 | _gl.blendFunc(_gl.ONE, _gl.ONE_MINUS_SRC_ALPHA); 1253 | _gl.getExtension("EXT_color_buffer_float"); 1254 | 1255 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _instances); 1256 | _gl.bufferData(_gl.ARRAY_BUFFER, _instanceBufferCapacity * 4, _gl.DYNAMIC_DRAW); 1257 | 1258 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _quad); 1259 | _gl.bufferData(_gl.ARRAY_BUFFER, new Float32Array([0, 0, 0, 1, 1, 1, 1, 0]), _gl.STATIC_DRAW); 1260 | 1261 | _gl.bindBuffer(_gl.UNIFORM_BUFFER, _ubo); 1262 | _gl.bufferData(_gl.UNIFORM_BUFFER, 48, _gl.DYNAMIC_DRAW); 1263 | _gl.bindBufferBase(_gl.UNIFORM_BUFFER, 0, _ubo); 1264 | 1265 | _gl.bindVertexArray(_vaoSprites); 1266 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _quad); 1267 | _gl.enableVertexAttribArray(0); 1268 | _gl.vertexAttribPointer(0, 2, _gl.FLOAT, false, 8, 0); 1269 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _instances); 1270 | _gl.enableVertexAttribArray(1); 1271 | _gl.vertexAttribDivisor(1, 1); 1272 | _gl.vertexAttribPointer(1, 4, _gl.FLOAT, false, 48, 0); 1273 | _gl.enableVertexAttribArray(2); 1274 | _gl.vertexAttribDivisor(2, 1); 1275 | _gl.vertexAttribPointer(2, 4, _gl.FLOAT, false, 48, 16); 1276 | _gl.enableVertexAttribArray(3); 1277 | _gl.vertexAttribDivisor(3, 1); 1278 | _gl.vertexAttribPointer(3, 4, _gl.FLOAT, false, 48, 32); 1279 | 1280 | _gl.bindVertexArray(_vaoLines); 1281 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _instances); 1282 | _gl.enableVertexAttribArray(0); 1283 | _gl.vertexAttribPointer(0, 4, _gl.FLOAT, false, 24, 0); 1284 | _gl.enableVertexAttribArray(1); 1285 | _gl.vertexAttribPointer(1, 2, _gl.FLOAT, false, 24, 16); 1286 | 1287 | _gl.bindVertexArray(_vaoMesh); 1288 | _gl.bindBuffer(_gl.ARRAY_BUFFER, _instances); 1289 | _gl.enableVertexAttribArray(0); 1290 | _gl.vertexAttribPointer(0, 4, _gl.FLOAT, false, 16, 0); 1291 | 1292 | _gl.bindVertexArray(null); 1293 | 1294 | this.bind(); 1295 | }; 1296 | 1297 | Myr.Color = function(r, g, b, a) { 1298 | this.r = r; 1299 | this.g = g; 1300 | this.b = b; 1301 | this.a = a === undefined?1:a; 1302 | }; 1303 | 1304 | Myr.Color.BLACK = new Myr.Color(0, 0, 0); 1305 | Myr.Color.BLUE = new Myr.Color(0, 0, 1); 1306 | Myr.Color.GREEN = new Myr.Color(0, 1, 0); 1307 | Myr.Color.CYAN = new Myr.Color(0, 1, 1); 1308 | Myr.Color.RED = new Myr.Color(1, 0, 0); 1309 | Myr.Color.MAGENTA = new Myr.Color(1, 0, 1); 1310 | Myr.Color.YELLOW = new Myr.Color(1, 1, 0); 1311 | Myr.Color.WHITE = new Myr.Color(1, 1, 1); 1312 | 1313 | Myr.Color.fromHex = hex => { 1314 | const integer = parseInt(hex, 16); 1315 | 1316 | if (hex.length === 6) 1317 | return new Myr.Color( 1318 | ((integer >> 16) & 0xFF) / 255, 1319 | ((integer >> 8) & 0xFF) / 255, 1320 | (integer & 0xFF) / 255); 1321 | else 1322 | return new Myr.Color( 1323 | ((integer >> 24) & 0xFF) / 255, 1324 | ((integer >> 16) & 0xFF) / 255, 1325 | ((integer >> 8) & 0xFF) / 255, 1326 | (integer & 0xFF) / 255); 1327 | }; 1328 | 1329 | Myr.Color.prototype.toHex = function() { 1330 | const componentToHex = component => { 1331 | const hex = component.toString(16); 1332 | 1333 | return hex.length === 1?"0" + hex:hex; 1334 | }; 1335 | 1336 | return "#" + 1337 | componentToHex(Math.round(this.r * 255)) + 1338 | componentToHex(Math.round(this.g * 255)) + 1339 | componentToHex(Math.round(this.b * 255)); 1340 | }; 1341 | 1342 | Myr.Color.fromHSV = (h, s, v) => { 1343 | const c = v * s; 1344 | const x = c * (1 - Math.abs((h * 6) % 2 - 1)); 1345 | const m = v - c; 1346 | 1347 | switch(Math.floor(h * 6)) { 1348 | case 1: 1349 | return new Myr.Color(x + m, c + m, m); 1350 | case 2: 1351 | return new Myr.Color(m, c + m, x + m); 1352 | case 3: 1353 | return new Myr.Color(m, x + m, c + m); 1354 | case 4: 1355 | return new Myr.Color(x + m, m, c + m); 1356 | case 5: 1357 | return new Myr.Color(c + m, m, x + m); 1358 | default: 1359 | return new Myr.Color(c + m, x + m, m); 1360 | } 1361 | }; 1362 | 1363 | Myr.Color.prototype.toHSV = function() { 1364 | const cMax = Math.max(this.r, this.g, this.b); 1365 | const cMin = Math.min(this.r, this.g, this.b); 1366 | let h, s, l = (cMax + cMin) * 0.5; 1367 | 1368 | if (cMax === cMin) 1369 | h = s = 0; 1370 | else { 1371 | let delta = cMax - cMin; 1372 | s = l > 0.5 ? delta / (2 - delta) : delta / (cMax + cMin); 1373 | 1374 | switch(cMax) { 1375 | case this.r: 1376 | h = (this.g - this.b) / delta + (this.g < this.b ? 6 : 0); 1377 | break; 1378 | case this.g: 1379 | h = (this.b - this.r) / delta + 2; 1380 | break; 1381 | case this.b: 1382 | h = (this.r - this.g) / delta + 4; 1383 | } 1384 | } 1385 | 1386 | return { 1387 | h: h / 6, 1388 | s: s, 1389 | v: cMax 1390 | }; 1391 | }; 1392 | 1393 | Myr.Color.prototype.copy = function() { 1394 | return new Myr.Color(this.r, this.g, this.b, this.a); 1395 | }; 1396 | 1397 | Myr.Color.prototype.add = function(color) { 1398 | this.r = Math.min(this.r + color.r, 1); 1399 | this.g = Math.min(this.g + color.g, 1); 1400 | this.b = Math.min(this.b + color.b, 1); 1401 | 1402 | return this; 1403 | }; 1404 | 1405 | Myr.Color.prototype.multiply = function(color) { 1406 | this.r *= color.r; 1407 | this.g *= color.g; 1408 | this.b *= color.b; 1409 | 1410 | return this; 1411 | }; 1412 | 1413 | Myr.Color.prototype.equals = function(color) { 1414 | return this.r === color.r && this.g === color.g && this.b === color.b && this.a === color.a; 1415 | }; 1416 | 1417 | Myr.Vector = function(x, y) { 1418 | this.x = x; 1419 | this.y = y; 1420 | }; 1421 | 1422 | Myr.Vector.prototype.copy = function() { 1423 | return new Myr.Vector(this.x, this.y); 1424 | }; 1425 | 1426 | Myr.Vector.prototype.add = function(vector) { 1427 | this.x += vector.x; 1428 | this.y += vector.y; 1429 | }; 1430 | 1431 | Myr.Vector.prototype.subtract = function(vector) { 1432 | this.x -= vector.x; 1433 | this.y -= vector.y; 1434 | }; 1435 | 1436 | Myr.Vector.prototype.negate = function() { 1437 | this.x = -this.x; 1438 | this.y = -this.y; 1439 | }; 1440 | 1441 | Myr.Vector.prototype.dot = function(vector) { 1442 | return this.x * vector.x + this.y * vector.y; 1443 | }; 1444 | 1445 | Myr.Vector.prototype.length = function() { 1446 | return Math.sqrt(this.dot(this)); 1447 | }; 1448 | 1449 | Myr.Vector.prototype.multiply = function(scalar) { 1450 | this.x *= scalar; 1451 | this.y *= scalar; 1452 | }; 1453 | 1454 | Myr.Vector.prototype.divide = function(scalar) { 1455 | if(scalar === 0) 1456 | this.x = this.y = 0; 1457 | else 1458 | this.multiply(1.0 / scalar); 1459 | }; 1460 | 1461 | Myr.Vector.prototype.normalize = function() { 1462 | this.divide(this.length()); 1463 | }; 1464 | 1465 | Myr.Vector.prototype.angle = function() { 1466 | return Math.atan2(this.y, this.x); 1467 | }; 1468 | 1469 | Myr.Vector.prototype.reflect = function(vector) { 1470 | const ddot = this.dot(vector) * 2; 1471 | 1472 | this.x -= ddot * vector.x; 1473 | this.y -= ddot * vector.y; 1474 | }; 1475 | 1476 | Myr.Vector.prototype.equals = function(vector) { 1477 | return this.x === vector.x && this.y === vector.y; 1478 | }; 1479 | 1480 | Myr.Vector.prototype.rotate = function(angle) { 1481 | const cos = Math.cos(angle); 1482 | const sin = Math.sin(angle); 1483 | const x = this.x; 1484 | const y = this.y; 1485 | 1486 | this.x = x * cos - y * sin; 1487 | this.y = x * sin + y * cos; 1488 | }; 1489 | 1490 | Myr.Transform = function(_00, _10, _20, _01, _11, _21) { 1491 | if(_00 === undefined) 1492 | this.identity(); 1493 | else { 1494 | this._00 = _00; 1495 | this._10 = _10; 1496 | this._20 = _20; 1497 | this._01 = _01; 1498 | this._11 = _11; 1499 | this._21 = _21; 1500 | } 1501 | }; 1502 | 1503 | Myr.Transform.prototype.apply = function(vector) { 1504 | const x = vector.x; 1505 | const y = vector.y; 1506 | 1507 | vector.x = this._00 * x + this._10 * y + this._20; 1508 | vector.y = this._01 * x + this._11 * y + this._21; 1509 | }; 1510 | 1511 | Myr.Transform.prototype.copy = function() { 1512 | return new Myr.Transform(this._00, this._10, this._20, this._01, this._11, this._21); 1513 | }; 1514 | 1515 | Myr.Transform.prototype.identity = function() { 1516 | this._00 = 1; 1517 | this._10 = 0; 1518 | this._20 = 0; 1519 | this._01 = 0; 1520 | this._11 = 1; 1521 | this._21 = 0; 1522 | }; 1523 | 1524 | Myr.Transform.prototype.set = function(transform) { 1525 | this._00 = transform._00; 1526 | this._10 = transform._10; 1527 | this._20 = transform._20; 1528 | this._01 = transform._01; 1529 | this._11 = transform._11; 1530 | this._21 = transform._21; 1531 | }; 1532 | 1533 | Myr.Transform.prototype.multiply = function(transform) { 1534 | const _00 = this._00; 1535 | const _10 = this._10; 1536 | const _01 = this._01; 1537 | const _11 = this._11; 1538 | 1539 | this._00 = _00 * transform._00 + _10 * transform._01; 1540 | this._10 = _00 * transform._10 + _10 * transform._11; 1541 | this._20 += _00 * transform._20 + _10 * transform._21; 1542 | this._01 = _01 * transform._00 + _11 * transform._01; 1543 | this._11 = _01 * transform._10 + _11 * transform._11; 1544 | this._21 += _01 * transform._20 + _11 * transform._21; 1545 | }; 1546 | 1547 | Myr.Transform.prototype.rotate = function(angle) { 1548 | const cos = Math.cos(angle); 1549 | const sin = Math.sin(angle); 1550 | 1551 | const _00 = this._00; 1552 | const _01 = this._01; 1553 | 1554 | this._00 = _00 * cos - this._10 * sin; 1555 | this._10 = _00 * sin + this._10 * cos; 1556 | this._01 = _01 * cos - this._11 * sin; 1557 | this._11 = _01 * sin + this._11 * cos; 1558 | }; 1559 | 1560 | Myr.Transform.prototype.shear = function(x, y) { 1561 | const _00 = this._00; 1562 | const _01 = this._01; 1563 | 1564 | this._00 += this._10 * y; 1565 | this._10 += _00 * x; 1566 | this._01 += this._11 * y; 1567 | this._11 += _01 * x; 1568 | }; 1569 | 1570 | Myr.Transform.prototype.translate = function(x, y) { 1571 | this._20 += this._00 * x + this._10 * y; 1572 | this._21 += this._01 * x + this._11 * y; 1573 | }; 1574 | 1575 | Myr.Transform.prototype.scale = function(x, y) { 1576 | this._00 *= x; 1577 | this._10 *= y; 1578 | this._01 *= x; 1579 | this._11 *= y; 1580 | }; 1581 | 1582 | Myr.Transform.prototype.invert = function() { 1583 | const s11 = this._00; 1584 | const s02 = this._10 * this._21 - this._11 * this._20; 1585 | const s12 = -this._00 * this._21 + this._01 * this._20; 1586 | 1587 | const d = 1.0 / (this._00 * this._11 - this._10 * this._01); 1588 | 1589 | this._00 = this._11 * d; 1590 | this._10 = -this._10 * d; 1591 | this._20 = s02 * d; 1592 | this._01 = -this._01 * d; 1593 | this._11 = s11 * d; 1594 | this._21 = s12 * d; 1595 | }; 1596 | 1597 | if(typeof module !== 'undefined') module.exports = Myr; --------------------------------------------------------------------------------