├── 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 | 
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 |
20 |
23 |
24 |
25 |
26 |
27 | | Quality |
28 |
29 |
38 | |
39 |
40 |
41 | | Render mode |
42 |
43 |
50 | |
51 |
52 |
53 | |
54 |
55 |
56 | |
57 |
58 |
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;
--------------------------------------------------------------------------------