diff --git a/KoiTranslations b/KoiTranslations
index 4a996f37..70994e10 160000
--- a/KoiTranslations
+++ b/KoiTranslations
@@ -1 +1 @@
-Subproject commit 4a996f3747635b9749223c1c893ee73303cc154d
+Subproject commit 70994e105bb43a460af7f89ec268c68a2aa4b6a7
diff --git a/css/book.css b/css/book.css
index 39c21aa3..1061c2e2 100644
--- a/css/book.css
+++ b/css/book.css
@@ -105,7 +105,7 @@
#book #spine .page .slot {
position: relative;
background-color: var(--book-page-slot-color);
- border-radius: var(--card-border-radius);
+ border-radius: calc(var(--card-border-radius) * var(--card-scale));
overflow: hidden;
}
@@ -121,6 +121,23 @@
box-shadow: none;
}
+#cards button:active svg {
+ filter: drop-shadow(0 0 var(--page-button-shadow-radius) var(--book-shadow-color));
+
+}
+
+#button-load-card {
+ position: absolute;
+ right: var(--book-button-padding);
+ top: calc(var(--button-height) * 2);
+}
+
+#button-home {
+ position: absolute;
+ right: var(--book-button-padding);
+ top: var(--button-height);
+}
+
#button-book {
position: absolute;
right: var(--book-button-padding);
@@ -187,3 +204,47 @@
.button-page.right {
right: calc(var(--button-width) * var(--page-button-page-shift) * -1);
}
+
+@media only screen and (orientation: portrait) {
+ #button-load-card {
+ position: absolute;
+ right: calc(var(--button-width) * 2 + var(--book-button-padding));
+ top: var(--book-button-padding);
+ }
+
+ #button-home {
+ position: absolute;
+ right: calc(var(--button-width) + var(--book-button-padding));
+ top: var(--book-button-padding);
+ }
+}
+
+@media only screen and (orientation: portrait) and (max-width: 600px), (orientation: portrait) and (max-height: 600px) {
+ #button-load-card {
+ position: absolute;
+ right: calc(var(--button-width-small) * 2 + var(--book-button-padding));
+ top: var(--book-button-padding);
+ }
+
+ #button-home {
+ position: absolute;
+ right: calc(var(--button-width-small) + var(--book-button-padding));
+ top: var(--book-button-padding);
+ }
+}
+
+@media only screen and (orientation: landscape) and (max-width: 600px),
+(orientation: landscape) and (max-height: 600px) {
+ #button-load-card {
+ position: absolute;
+ right: var(--book-button-padding);
+ top: calc(var(--button-height-small) * 2);
+ }
+
+ #button-home {
+ position: absolute;
+ right: var(--book-button-padding);
+ top: var(--button-height-small);
+ }
+
+}
\ No newline at end of file
diff --git a/css/buttons.css b/css/buttons.css
index 176af954..4eeae1ad 100644
--- a/css/buttons.css
+++ b/css/buttons.css
@@ -1,7 +1,11 @@
:root {
--button-width: 80px;
--button-height: 80px;
+
--load-card-button-padding: 10px;
+
+ --button-height-small: 3.5rem;
+ --button-width-small: 3.5rem;
}
button {
@@ -18,17 +22,29 @@ button {
font-size: 18pt;
font-family: inherit;
user-select: none;
+ -webkit-tap-highlight-color: transparent;
}
-.load-card-button {
+.home-button {
position: absolute;
- left: 0;
+ right: 0;
top: 0;
- width: calc(var(--button-width) * 2);
- background-color: var(--code-button-color);
- box-shadow: 0 0 var(--code-shadow-radius) var(--code-shadow-color);
- padding: 0 var(--load-card-button-padding);
- margin-top: var(--code-button-margin);
- margin-left: var(--code-button-margin);
- border-radius: var(--code-button-radius);
+
+}
+
+@media (max-width: 600px) or (max-height: 600px){
+ button {
+ font-size: 14pt;
+ width: var(--button-width-small);
+ height: var(--button-height-small);
+ }
+
+ .button-page.left {
+ left: calc(var(--button-width-small) * var(--page-button-page-shift) * -1);
}
+
+ .button-page.right {
+ right: calc(var(--button-width-small) * var(--page-button-page-shift) * -1);
+ }
+
+}
\ No newline at end of file
diff --git a/css/card.css b/css/card.css
index 4a3e5233..cc649210 100644
--- a/css/card.css
+++ b/css/card.css
@@ -4,11 +4,18 @@
--card-color-background-graphics: #7e605e;
--card-color-drop-target: #ffffff55;
+ --card-scale: 1;
+
+ --card-width-default: 250px;
+ --card-height-default: 350px;
+ --card-preview-width-default: 200px;
+ --card-preview-height-default: 120px;
+
/* Dimensions */
- --card-width: 250px;
- --card-height: 350px;
- --card-preview-width: 200px;
- --card-preview-height: 120px;
+ --card-width: calc(var(--card-width-default) * var(--card-scale));
+ --card-height: calc(var(--card-height-default) * var(--card-scale));
+ --card-preview-width: calc(var(--card-preview-width-default) * var(--card-scale));
+ --card-preview-height: calc(var(--card-preview-height-default) * var(--card-scale));
--card-preview-margin: calc((var(--card-width) - var(--card-preview-width) - 2 * var(--card-preview-border)) * 0.5);
--card-preview-columns: 6;
--card-preview-rows: 10;
@@ -30,6 +37,8 @@
--card-code-icon-radius: 32px;
--card-code-icon-margin: 8px;
--card-code-opacity: 0.5;
+
+ --font-size: 20px;
}
.card-shape {
diff --git a/css/code.css b/css/code.css
index e7df75e9..52099a56 100644
--- a/css/code.css
+++ b/css/code.css
@@ -52,6 +52,13 @@
pointer-events: auto;
}
+@media (max-height: 600px) {
+ #code .view canvas {
+ width: 50%;
+ height: 50%;
+ }
+}
+
#code .view button {
width: 50%;
background-color: var(--code-button-color);
diff --git a/css/loader.css b/css/loader.css
index f510147b..da2b911a 100644
--- a/css/loader.css
+++ b/css/loader.css
@@ -27,7 +27,7 @@
align-items: center;
justify-content: center;
flex-direction: column;
- transition: var(--loader-fade-out);
+ transition: var(--loader-fade-out) opacity;
}
#loader canvas {
@@ -39,29 +39,56 @@
pointer-events: none;
}
-#loader #loader-discord {
+#loader-links {
position: absolute;
- right: 0;
+ left: 0;
bottom: 0;
- width: 400px;
- height: 136px;
- transition: var(--loader-fade-in);
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: flex-end;
+ justify-content: flex-end;
+ width: 100%;
+ height: 4rem;
+ max-height: 100px;
+ padding: 16px;
+}
+
+#loader-links .loader-icon {
+ width: 4rem;
+ margin: auto 0;
+ transition: var(--loader-fade-in) opacity;
transition-timing-function: ease-in;
}
-#loader #loader-discord path {
+#loader-links .loader-icon svg {
+ width: 100%;
+ height: 100%;
+}
+
+#loader-links:hover .loader-bar {
+ opacity: 100%;
+}
+
+#loader #loader-discord path,
+#loader #loader-website path {
fill: #FEFFFF;
}
-#loader #loader-discord.invisible {
+#loader .loader-icon.invisible,
+#loader .loader-bar.invisible {
opacity: 0;
}
-#loader #loader-discord:hover {
+#loader #loader-discord:hover,
+#loader #loader-website:hover {
cursor: pointer;
}
-#loader #loader-discord:hover path {
+#loader #loader-website:hover path {
+ fill: #fca938;
+}
+
+#loader #loader-discord:hover path{
fill: #778BD8;
}
@@ -130,8 +157,10 @@
#loader-graphics {
display: flex;
justify-content: center;
+ align-items: center;
width: 100%;
margin-bottom: var(--loader-logo-margin);
+ flex-direction: column;
}
#loader-slots {
@@ -139,6 +168,8 @@
justify-content: center;
width: 100%;
height: 20%;
+ align-items: center;
+ align-content: center;
}
#loader-slots .loader-slot {
@@ -174,9 +205,30 @@
margin-bottom: var(--loader-button-margin);
}
+.loader-bar {
+ height: var(--loader-logo-width);
+ border-radius: 5px;
+ margin: auto 0;
+ align-self: flex-start;
+ justify-self: flex-start;
+ transition: var(--loader-fade-in) opacity;
+ transition-timing-function: ease-in;
+ cursor: pointer;
+ opacity: 0;
+}
+
+.loader-bar svg {
+ width: 100%;
+ height: 100%;
+}
+
+.loader-bar:hover {
+ filter: invert(100%);
+}
+
#loader-icon {
width: var(--loader-logo-width);
- transition: var(--loader-fade-in);
+ transition: var(--loader-fade-in) opacity;
transition-timing-function: ease-in;
}
@@ -205,7 +257,7 @@
justify-content: center;
margin-bottom: var(--loader-button-margin);
opacity: 0;
- transition: var(--loader-button-fade-in);
+ transition: var(--loader-button-fade-in) opacity;
transition-timing-function: ease-in;
}
@@ -215,7 +267,7 @@
.loader-button button {
width: 100%;
- transition: 0.5s;
+ transition: 0.5s opacity;
background-color: var(--color-white);
color: var(--loader-button-text-color);
font-weight: bold;
@@ -238,4 +290,100 @@
background-color: var(--color-white);
color: var(--loader-button-text-color);
transition: 0s;
+}
+
+#loader-loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ transition: 0.5s opacity;
+ position: absolute;
+ top: 20px;
+}
+
+#loader-loading.invisible {
+ opacity: 0;
+ animation: none;
+}
+
+#loader-loading-text {
+ color: var(--color-white);
+ font-size: 1.5em;
+ margin-left: 5px;
+}
+
+#loader-loading-icon{
+ width: 20px;
+ animation: flutter 1s ease-in-out infinite;
+ transform-origin: top left;
+}
+
+@media only screen and (orientation: portrait) {
+ #loader-slots {
+ flex-direction: column;
+ height: unset;
+
+ }
+
+ #loader-slots .loader-slot {
+ flex-direction: row;
+ width: 60%;
+ }
+
+ #loader-slots .loader-slot h1 {
+ color: var(--color-white);
+ position: absolute;
+ left: -60px;
+ bottom: 16px;
+ top: unset;
+ width: 60px;
+ height: 60px;
+ user-select: none;
+ background-color: var(--loader-button-text-color);
+ border-top-left-radius: 60px;
+ border-bottom-left-radius: 60px;
+ display: flex;
+ justify-content: flex-end;
+ flex-direction: column;
+ margin: 0;
+ text-align: center;
+ line-height: 60px;
+ padding: 0;
+ }
+
+ #loader-slots .loader-slot button:nth-child(2) {
+ border-bottom-left-radius: 0;
+ }
+
+ #loader-slots .loader-slot button:not(:last-child) {
+ margin-right: 10px;
+ }
+
+ .loader-button {
+ width: 60%;
+ }
+}
+
+@keyframes flutter {
+ 0% {
+ transform: skew(0, 0) scale(1);
+ }
+
+ 20% {
+ transform: translateY(100%) skew(-10deg, -10deg) scale(1) translateY(-100%);
+ }
+
+ 40% {
+ transform: translateY(100%) skew(10deg, 10deg) scale(1.1) translateY(-100%);
+ }
+
+ 60% {
+ transform: translateY(100%) skew(-5deg, -5deg) scale(.9) translateY(-100%);
+ }
+
+ 80% {
+ transform: translateY(100%) skew(0deg, 0deg) scale(1) translateY(-100%);
+ }
+
}
\ No newline at end of file
diff --git a/css/menu.css b/css/menu.css
index 9706e88c..a4b7c941 100644
--- a/css/menu.css
+++ b/css/menu.css
@@ -4,6 +4,7 @@
--menu-box-spacing: 16px;
--menu-button-color: var(--color-water-deep);
--menu-button-border-radius: var(--loader-button-border-radius);
+ --menu-font-size: 1rem;
}
#menu {
@@ -41,6 +42,7 @@
#menu-box h1 {
color: var(--color-white);
+ margin: unset;
}
#menu-box button {
@@ -72,12 +74,21 @@
#menu-box input[type=checkbox] {
float: left;
+ min-width: 1rem;
+ min-height: 1rem;
}
#menu-box select {
width: 100%;
+ min-height: 1.5rem;
}
#menu-box table {
text-align: right;
+}
+
+@media screen and (max-width: 600px) or (max-height: 600px){
+ #menu-box button {
+ max-height: var(--button-height-small);
+ }
}
\ No newline at end of file
diff --git a/css/overlay.css b/css/overlay.css
index 954de70f..485bcf3f 100644
--- a/css/overlay.css
+++ b/css/overlay.css
@@ -100,4 +100,19 @@
animation: arrow-bounce-up var(--overlay-arrow-animation-time) infinite;
animation-timing-function: ease-in-out;
transform: scale(var(--overlay-arrow-scale), 1) rotate(45deg);
+}
+
+.skip-button {
+ pointer-events: auto;
+ width: auto;
+ min-width: var(--button-width);
+ user-select: auto;
+ position: absolute;
+ left: 10px;
+ bottom: var(--overlay-text-top);
+ background: #325c73cc;
+ color: var(--color-white);
+ font-size: 1.5rem;
+ border-radius: 16px;
+ border: 2px solid var(--color-white);
}
\ No newline at end of file
diff --git a/css/style.css b/css/style.css
index ddfd0c75..cf8473aa 100644
--- a/css/style.css
+++ b/css/style.css
@@ -1,6 +1,11 @@
+:root {
+ --padding-top-notch: env(safe-area-inset-top);
+}
+
body {
margin: 0;
font-family: Grandstander;
+ background: var(--color-shrubbery-leaf);
}
#wrapper {
@@ -34,4 +39,12 @@ body {
top: 0;
width: 100%;
height: 100%;
+}
+
+/* Adjust padding for devices with a notch */
+@media only screen and (orientation: portrait) {
+ .notch-padded {
+ padding-top: max(env(safe-area-inset-top), 30px);
+ padding-bottom: env(safe-area-inset-bottom); /* iOS 11.2+ */
+ }
}
\ No newline at end of file
diff --git a/index.html b/index.html
index c2fc78fb..81545898 100644
--- a/index.html
+++ b/index.html
@@ -7,6 +7,10 @@
font-family: "Grandstander";
src: url("font/Grandstander.ttf");
}
+
+ body {
+ background: #4b812e;
+ }
@@ -22,12 +26,13 @@
-
+
+
-
+
@@ -42,17 +47,25 @@
+
+
+
+
+
+
+
+
@@ -290,6 +303,8 @@
+
+
diff --git a/js/koi/code/codeViewer.js b/js/koi/code/codeViewer.js
index 40fc7d37..66993a18 100644
--- a/js/koi/code/codeViewer.js
+++ b/js/koi/code/codeViewer.js
@@ -42,6 +42,15 @@ CodeViewer.prototype.createButtonCopy = function(image, audio) {
return button;
};
+/**
+ * Create a file name
+ * @returns {String} The file name.
+ */
+CodeViewer.prototype.createFileName = function() {
+ const datetimeString = new Date().toISOString().replace(/:/g, "-");
+ return `koi-${datetimeString}.png`;
+}
+
/**
* Create the download button
* @param {HTMLCanvasElement} image The fish code image
@@ -64,7 +73,7 @@ CodeViewer.prototype.createButtonDownload = function(image, audio) {
window.webkit.messageHandlers.saveImage.postMessage({blob: base64data});
}
} else {
- this.storage.imageToFile(blob, this.DEFAULT_NAME)
+ this.storage.imageToFile(blob, this.createFileName())
}
});
diff --git a/js/koi/drop.js b/js/koi/drop.js
index 97aa1585..8ecd991a 100644
--- a/js/koi/drop.js
+++ b/js/koi/drop.js
@@ -62,7 +62,7 @@ Drop.prototype.drop = function(event) {
if (this.gui.cards.hand.isFull())
break;
- this.dropFile(file, new Vector2(event.clientX, event.clientY));
+ this.dropFile(file, new Vector2(event.clientX || window.screen.width / 2, event.clientY || window.screen.height / 2));
}
}
};
@@ -73,12 +73,22 @@ Drop.prototype.drop = function(event) {
* @param {Vector2} target The position the file was dropped at
*/
Drop.prototype.dropFile = function(file, target) {
- if (!this.IMAGE_TYPES.includes(file.type))
+ if (file.type && !this.IMAGE_TYPES.includes(file.type) || file.mimeType && !this.IMAGE_TYPES.includes(file.mimeType)) {
+ if (Capacitor && Capacitor.Plugins && Capacitor.isPluginAvailable('Toast'))
+ Capacitor.Plugins.Toast.show({text: "Could not load koi"});
+
return;
+ }
- const reader = new FileReader();
+ if (file.data && file.data.length > 0) {
+ let base64Data;
+
+ if (file.data.startsWith('data:image')) {
+ base64Data = file.data;
+ } else {
+ base64Data = `data:${file.mimeType};base64,` + file.data;
+ }
- reader.onload = event => {
const image = new Image();
image.onload = () => {
@@ -101,11 +111,49 @@ Drop.prototype.dropFile = function(file, target) {
this.gui.cards.add(card);
}
+ else {
+ if (Capacitor && Capacitor.Plugins && Capacitor.isPluginAvailable('Toast'))
+ Capacitor.Plugins.Toast.show({text: "Could not load koi"});
+ }
+ } else {
+ if (Capacitor && Capacitor.Plugins && Capacitor.isPluginAvailable('Toast'))
+ Capacitor.Plugins.Toast.show({text: "Hand is full"});
}
};
- image.src = event.target.result;
- };
+ image.src = base64Data;
+ } else {
+ const reader = new FileReader();
+
+ reader.onload = event => {
+ const image = new Image();
+
+ image.onload = () => {
+ if (!this.gui.cards.hand.isFull()) {
+ const body = new CodeReader(image).read();
+
+ if (body) {
+ const buffer = new BinBuffer();
- reader.readAsDataURL(file);
+ body.pattern.serialize(buffer);
+ body.initializeSpine(new Vector2(), new Vector2(1, 0));
+
+ const card = new Card(body, target, 0);
+
+ card.initialize(
+ this.systems.preview,
+ this.systems.atlas,
+ this.systems.bodies,
+ this.systems.randomSource);
+
+ this.gui.cards.add(card);
+ }
+ }
+ };
+
+ image.src = event.target.result;
+ };
+
+ reader.readAsDataURL(file);
+ }
};
\ No newline at end of file
diff --git a/js/koi/fish/pattern/layer/layerRidge.js b/js/koi/fish/pattern/layer/layerRidge.js
index b6065ae0..9a433be8 100644
--- a/js/koi/fish/pattern/layer/layerRidge.js
+++ b/js/koi/fish/pattern/layer/layerRidge.js
@@ -41,7 +41,7 @@ LayerRidge.prototype.SHADER_VERTEX = `#version 100
attribute vec2 position;
attribute vec2 uv;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
iUv = uv;
@@ -53,25 +53,25 @@ void main() {
LayerRidge.prototype.SHADER_FRAGMENT = `#version 100
` + CommonShaders.cubicNoise3 + `
uniform lowp vec3 color;
-uniform mediump float scale;
-uniform mediump float power;
-uniform mediump float threshold;
-uniform mediump float focus;
-uniform mediump float focusPower;
-uniform mediump vec2 size;
+uniform highp float scale;
+uniform highp float power;
+uniform highp float threshold;
+uniform highp float focus;
+uniform highp float focusPower;
+uniform highp vec2 size;
uniform highp vec3 origin;
uniform highp mat3 rotate;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
#define RIDGE_ATTENUATION 1.4
#define ATTENUATION 2.0
void main() {
- mediump float phaseThreshold = pow(1.0 - RIDGE_ATTENUATION * abs(iUv.y - 0.5), power);
+ highp float phaseThreshold = pow(1.0 - RIDGE_ATTENUATION * abs(iUv.y - 0.5), power);
highp vec2 at = (iUv - vec2(0.5)) * size * scale;
- mediump float noise = cubicNoise(origin + vec3(at, 0.0) * rotate);
- mediump float strength = pow(max(0.0, 1.0 - ATTENUATION * abs(iUv.x - focus)), focusPower);
+ highp float noise = cubicNoise(origin + vec3(at, 0.0) * rotate);
+ highp float strength = pow(max(0.0, 1.0 - ATTENUATION * abs(iUv.x - focus)), focusPower);
if (noise > phaseThreshold * strength)
discard;
diff --git a/js/koi/fish/pattern/layer/layerShapeBody.js b/js/koi/fish/pattern/layer/layerShapeBody.js
index 4354a341..69694a76 100644
--- a/js/koi/fish/pattern/layer/layerShapeBody.js
+++ b/js/koi/fish/pattern/layer/layerShapeBody.js
@@ -37,21 +37,21 @@ void main() {
`;
LayerShapeBody.prototype.SHADER_FRAGMENT = `#version 100
-uniform mediump float centerPower;
-uniform mediump float radiusPower;
-uniform mediump float eyePosition;
-uniform mediump float shadePower;
-uniform mediump float lightPower;
-uniform mediump float ambient;
-uniform mediump vec2 size;
+uniform highp float centerPower;
+uniform highp float radiusPower;
+uniform highp float eyePosition;
+uniform highp float shadePower;
+uniform highp float lightPower;
+uniform highp float ambient;
+uniform highp vec2 size;
uniform lowp vec3 shadeColor;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
#define EYE_SHADE_PUPIL 0.2
#define EYE_RADIUS_PUPIL 0.1
-mediump float getRadius(mediump float x) {
+highp float getRadius(highp float x) {
return pow(cos(3.141592 * (pow(x, centerPower) - 0.5)), radiusPower);
}
diff --git a/js/koi/fish/pattern/layer/layerShapeFin.js b/js/koi/fish/pattern/layer/layerShapeFin.js
index 54b1b24d..c89e91e9 100644
--- a/js/koi/fish/pattern/layer/layerShapeFin.js
+++ b/js/koi/fish/pattern/layer/layerShapeFin.js
@@ -24,11 +24,12 @@ LayerShapeFin.prototype.SAMPLER_DIPS = new SamplerPower(.25, 3, 1.5);
LayerShapeFin.prototype.SAMPLER_DIP_POWER = new SamplerPower(.5, 2, .7);
LayerShapeFin.prototype.SAMPLER_ROUNDNESS = new SamplerPower(.05, .25, .6);
+// language=glsl
LayerShapeFin.prototype.SHADER_VERTEX = `#version 100
attribute vec2 position;
attribute vec2 uv;
-uniform mediump float angle;
+uniform highp float angle;
varying vec2 iUv;
varying float iBeta;
@@ -46,25 +47,26 @@ void main() {
}
`;
+// language=glsl
LayerShapeFin.prototype.SHADER_FRAGMENT = `#version 100
-uniform mediump float angle;
-uniform mediump float inset;
-uniform mediump float dips;
-uniform mediump float dipPower;
-uniform mediump float roundness;
+uniform highp float angle;
+uniform highp float inset;
+uniform highp float dips;
+uniform highp float dipPower;
+uniform highp float roundness;
-varying mediump vec2 iUv;
-varying mediump float iBeta;
-varying mediump float iCutaway;
-varying mediump float iFinRadius;
+varying highp vec2 iUv;
+varying highp float iBeta;
+varying highp float iCutaway;
+varying highp float iFinRadius;
#define ALPHA 0.8
void main() {
- mediump float radiusProgress = clamp((atan(iUv.y, iUv.x) - iBeta) / angle, 0.0, 1.0);
- mediump float radiusMultiplier = 1.0 - inset + inset * pow(cos(radiusProgress * 6.283185 * dips) * 0.5 + 0.5, dipPower);
- mediump float radius = length(iUv);
- mediump float roundnessMultiplier = pow(sin(radiusProgress * 3.141593), roundness);
+ highp float radiusProgress = clamp((atan(iUv.y, iUv.x) - iBeta) / angle, 0.0, 1.0);
+ highp float radiusMultiplier = 1.0 - inset + inset * pow(cos(radiusProgress * 6.283185 * dips) * 0.5 + 0.5, dipPower);
+ highp float radius = length(iUv);
+ highp float roundnessMultiplier = pow(sin(radiusProgress * 3.141593), roundness);
if (radius > iFinRadius * radiusMultiplier * roundnessMultiplier ||
iUv.x < sqrt(iUv.y) * iCutaway ||
diff --git a/js/koi/fish/pattern/layer/layerSpots.js b/js/koi/fish/pattern/layer/layerSpots.js
index afadcf44..c1e9cca7 100644
--- a/js/koi/fish/pattern/layer/layerSpots.js
+++ b/js/koi/fish/pattern/layer/layerSpots.js
@@ -53,26 +53,27 @@ void main() {
}
`;
+// language=glsl
LayerSpots.prototype.SHADER_FRAGMENT = `#version 100
` + CommonShaders.cubicNoise3 + `
uniform lowp vec3 color;
-uniform mediump float scale;
-uniform mediump float stretch;
-uniform mediump float threshold;
-uniform mediump vec2 focus;
-uniform mediump float power;
-uniform mediump vec2 size;
+uniform highp float scale;
+uniform highp float stretch;
+uniform highp float threshold;
+uniform highp vec2 focus;
+uniform highp float power;
+uniform highp vec2 size;
uniform highp vec3 anchor;
uniform highp mat3 rotate;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
#define ATTENUATION 1.5
void main() {
highp vec2 at = vec2(iUv.x * stretch - 0.5, iUv.y - 0.5) * size * scale;
- mediump float noise = cubicNoise(anchor + vec3(at, 0.0) * rotate);
- mediump float strength = pow(max(0.0, 1.0 - ATTENUATION * length(iUv - focus)), power);
+ highp float noise = cubicNoise(anchor + vec3(at, 0.0) * rotate);
+ highp float strength = pow(max(0.0, 1.0 - ATTENUATION * length(iUv - focus)), power);
if (noise > threshold * strength)
discard;
diff --git a/js/koi/fish/pattern/layer/layerStripes.js b/js/koi/fish/pattern/layer/layerStripes.js
index 9513527c..c8ff3251 100644
--- a/js/koi/fish/pattern/layer/layerStripes.js
+++ b/js/koi/fish/pattern/layer/layerStripes.js
@@ -63,28 +63,28 @@ void main() {
LayerStripes.prototype.SHADER_FRAGMENT = `#version 100
` + CommonShaders.cubicNoise3 + `
uniform lowp vec3 color;
-uniform mediump float scale;
-uniform mediump float distortion;
-uniform mediump float roughness;
-uniform mediump float threshold;
-uniform mediump float slant;
-uniform mediump float suppression;
-uniform mediump float focus;
-uniform mediump float power;
-uniform mediump vec2 size;
+uniform highp float scale;
+uniform highp float distortion;
+uniform highp float roughness;
+uniform highp float threshold;
+uniform highp float slant;
+uniform highp float suppression;
+uniform highp float focus;
+uniform highp float power;
+uniform highp vec2 size;
uniform highp vec3 anchor;
uniform highp mat3 rotate;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
#define ATTENUATION 2.0
void main() {
highp vec2 at = (iUv - 0.5) * size * roughness;
- mediump float dx = cubicNoise(anchor + vec3(at, 0.0) * rotate);
- mediump float dy = 2.0 * abs(iUv.y - 0.5);
- mediump float x = 2.0 * scale * iUv.x + dx * distortion / scale - dy * dy * slant;
- mediump float strength = pow(max(0.0, 1.0 - ATTENUATION * abs(iUv.x - focus)), power);
+ highp float dx = cubicNoise(anchor + vec3(at, 0.0) * rotate);
+ highp float dy = 2.0 * abs(iUv.y - 0.5);
+ highp float x = 2.0 * scale * iUv.x + dx * distortion / scale - dy * dy * slant;
+ highp float strength = pow(max(0.0, 1.0 - ATTENUATION * abs(iUv.x - focus)), power);
if (min(mod(x, 2.0), 2.0 - mod(x, 2.0)) + dy * dy * suppression > threshold * strength)
discard;
diff --git a/js/koi/fish/pattern/layer/layerWeb.js b/js/koi/fish/pattern/layer/layerWeb.js
index 61a83d5c..a208669e 100644
--- a/js/koi/fish/pattern/layer/layerWeb.js
+++ b/js/koi/fish/pattern/layer/layerWeb.js
@@ -27,6 +27,7 @@ LayerWeb.prototype.SAMPLER_SCALE = new SamplerPlateau(1.5, 3, 6.5, .7);
LayerWeb.prototype.SAMPLER_THICKNESS = new SamplerPlateau(.1, .15, .3, .7);
LayerWeb.prototype.SAMPLER_THRESHOLD = new SamplerPlateau(.3, .5, .7, .6);
+// language=glsl
LayerWeb.prototype.SHADER_VERTEX = `#version 100
attribute vec2 position;
attribute vec2 uv;
@@ -40,21 +41,22 @@ void main() {
}
`;
+// language=glsl
LayerWeb.prototype.SHADER_FRAGMENT = `#version 100
` + CommonShaders.cubicNoise3 + `
uniform lowp vec3 color;
-uniform mediump float scale;
-uniform mediump float thickness;
-uniform mediump float threshold;
-uniform mediump vec2 size;
+uniform highp float scale;
+uniform highp float thickness;
+uniform highp float threshold;
+uniform highp vec2 size;
uniform highp vec3 anchor;
uniform highp mat3 rotate;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
highp vec2 at = (iUv - 0.5) * size * scale;
- mediump float noise = cubicNoise(anchor + vec3(at, 0.0) * rotate);
+ highp float noise = cubicNoise(anchor + vec3(at, 0.0) * rotate);
if (noise < threshold - thickness * 0.5 || noise > threshold + thickness * 0.5)
discard;
diff --git a/js/koi/gui/cards/buttons/cardLoadButton.js b/js/koi/gui/cards/buttons/cardLoadButton.js
new file mode 100644
index 00000000..54f1c618
--- /dev/null
+++ b/js/koi/gui/cards/buttons/cardLoadButton.js
@@ -0,0 +1,42 @@
+/**
+ * A card load button
+ * @param {Function} onClick The function to execute on click
+ * @constructor
+ */
+const CardLoadButton = function(onClick) {
+ this.element = this.makeElement(onClick);
+};
+
+CardLoadButton.prototype.ID = "button-load-card";
+CardLoadButton.prototype.FILE = "svg/add.svg";
+CardLoadButton.prototype.WIDTH = 10;
+CardLoadButton.prototype.HEIGHT = 10;
+
+
+CardLoadButton.prototype.loadSVG = function() {
+ const request = new XMLHttpRequest();
+
+ request.onload = () => {
+ this.element.innerHTML = request.responseText;
+ };
+
+ request.open("GET", this.FILE, true);
+ request.send();
+}
+
+
+/**
+ * Make the button element
+ * @param {Function} onClick The function to execute on click
+ * @returns {HTMLButtonElement} The button element
+ */
+CardLoadButton.prototype.makeElement = function(onClick) {
+ const element = document.createElement("button");
+
+ element.onclick = onClick;
+ element.id = this.ID;
+
+ this.loadSVG();
+
+ return element;
+};
\ No newline at end of file
diff --git a/js/koi/gui/cards/buttons/cardMenuButton.js b/js/koi/gui/cards/buttons/cardMenuButton.js
new file mode 100644
index 00000000..a0aac4e0
--- /dev/null
+++ b/js/koi/gui/cards/buttons/cardMenuButton.js
@@ -0,0 +1,42 @@
+/**
+ * A card menu button
+ * @param {Function} onClick The function to execute on click
+ * @constructor
+ */
+const CardMenuButton = function(onClick) {
+ this.element = this.makeElement(onClick);
+};
+
+CardMenuButton.prototype.ID = "button-home";
+CardMenuButton.prototype.FILE = "svg/settings.svg";
+CardMenuButton.prototype.WIDTH = 10;
+CardMenuButton.prototype.HEIGHT = 10;
+
+
+CardMenuButton.prototype.loadSVG = function() {
+ const request = new XMLHttpRequest();
+
+ request.onload = () => {
+ this.element.innerHTML = request.responseText;
+ };
+
+ request.open("GET", this.FILE, true);
+ request.send();
+}
+
+
+/**
+ * Make the button element
+ * @param {Function} onClick The function to execute on click
+ * @returns {HTMLButtonElement} The button element
+ */
+CardMenuButton.prototype.makeElement = function(onClick) {
+ const element = document.createElement("button");
+
+ element.onclick = onClick;
+ element.id = this.ID;
+
+ this.loadSVG();
+
+ return element;
+};
\ No newline at end of file
diff --git a/js/koi/gui/cards/card.js b/js/koi/gui/cards/card.js
index 9446f2a6..5ffa3c71 100644
--- a/js/koi/gui/cards/card.js
+++ b/js/koi/gui/cards/card.js
@@ -36,8 +36,11 @@ Card.prototype.CLASS_INFO_BACKGROUND = "background";
Card.prototype.CLASS_INFO_LABEL = "label";
Card.prototype.CLASS_INFO_VALUE = "value";
Card.prototype.CLASS_JAPANESE = "japanese";
-Card.prototype.WIDTH = StyleUtils.getInt("--card-width");
-Card.prototype.HEIGHT = StyleUtils.getInt("--card-height");
+Card.prototype.SCALE = StyleUtils.getFloat("--card-scale");
+Card.prototype.WIDTH_DEFAULT = StyleUtils.getInt("--card-width-default");
+Card.prototype.HEIGHT_DEFAULT = StyleUtils.getInt("--card-height-default");
+Card.prototype.WIDTH = Card.prototype.WIDTH_DEFAULT * Card.prototype.SCALE;
+Card.prototype.HEIGHT = Card.prototype.HEIGHT_DEFAULT * Card.prototype.SCALE;
Card.prototype.RATIO = Card.prototype.WIDTH / Card.prototype.HEIGHT;
Card.prototype.LANG_WEIGHT = "INFO_WEIGHT";
Card.prototype.LANG_LENGTH = "INFO_LENGTH";
diff --git a/js/koi/gui/cards/cardBook.js b/js/koi/gui/cards/cardBook.js
index 1656b635..84d966e6 100644
--- a/js/koi/gui/cards/cardBook.js
+++ b/js/koi/gui/cards/cardBook.js
@@ -7,7 +7,7 @@
* @param {Function} onUnlock A function to call when a new page is unlocked
* @constructor
*/
-const CardBook = function(width, height, cards, audio, onUnlock) {
+const CardBook = function(width, height, cards, audio, onUnlock, animate=true) {
this.spine = this.createSpine();
this.element = this.createElement(this.spine);
this.width = width;
@@ -23,6 +23,7 @@ const CardBook = function(width, height, cards, audio, onUnlock) {
this.onUnlock = onUnlock;
this.unlocked = 0;
this.invisibleTimeout = null;
+ this.animate = animate;
this.populateSpine();
@@ -38,6 +39,7 @@ CardBook.prototype.CLASS_HIDDEN = "hidden";
CardBook.prototype.CLASS_INVISIBLE = "invisible";
CardBook.prototype.HIDE_TIME = StyleUtils.getFloat("--book-hide-time");
CardBook.prototype.PADDING_TOP = .05;
+CardBook.prototype.PADDING_TOP_PORTRAIT = .2;
CardBook.prototype.PADDING_PAGE = .02;
CardBook.prototype.PADDING_CARD = .035;
CardBook.prototype.HEIGHT = .65;
@@ -412,6 +414,8 @@ CardBook.prototype.removeFromBook = function(card) {
* Fit the book and its contents to the view size
*/
CardBook.prototype.fit = function() {
+ let scale = 1;
+
const pageHeight = Math.round(this.height * this.HEIGHT * (1 - 2 * this.PADDING_PAGE));
const cardPadding = Math.round(pageHeight * this.PADDING_CARD);
const cardHeight = Math.round((pageHeight - 3 * cardPadding) * .5);
@@ -419,14 +423,22 @@ CardBook.prototype.fit = function() {
const pageWidth = cardWidth * 2 + cardPadding * 3;
const bookWidth = pageWidth * 2 + Math.round(this.height * this.HEIGHT * this.PADDING_PAGE * 2);
- this.element.style.width = bookWidth + "px";
- this.element.style.height = this.height * this.HEIGHT + "px";
- this.element.style.left = (this.width - bookWidth) * .5 + "px";
+ if (this.width < this.height) {
+ scale = this.width / bookWidth * .7;
+
+ this.element.style.top = this.height * this.PADDING_TOP_PORTRAIT + "px";
+ } else {
+ this.element.style.top = this.height * this.PADDING_TOP + "px";
+ }
+
+ this.element.style.width = bookWidth * scale + "px";
+ this.element.style.height = this.height * this.HEIGHT * scale + "px";
+ this.element.style.left = (this.width - bookWidth * scale) * .5 + "px";
this.element.style.top = this.height * this.PADDING_TOP + "px";
- this.spine.style.height = pageHeight + "px";
+ this.spine.style.height = pageHeight * scale + "px";
for (const page of this.pages)
- page.fit(cardWidth, cardHeight, cardPadding);
+ page.fit(cardWidth * scale, cardHeight * scale, cardPadding * scale);
};
/**
diff --git a/js/koi/gui/cards/cards.js b/js/koi/gui/cards/cards.js
index 3378d78d..3359bf7e 100644
--- a/js/koi/gui/cards/cards.js
+++ b/js/koi/gui/cards/cards.js
@@ -11,7 +11,14 @@ const Cards = function(element, codeViewer, audio) {
this.audio = audio;
this.dropTarget = this.createDropTarget();
this.buttonBook = new CardBookButton(this.toggleBook.bind(this));
+ this.buttonLoadCard = new CardLoadButton(() => {
+ storage.loadImage();
+ });
+ this.buttonMenu = new CardMenuButton(() => {
+ menu.toggle();
+ });
this.bookEnabled = false;
+ this.buttonsShown = false;
this.book = new CardBook(element.clientWidth, element.clientHeight, this, audio, () => {
if (this.koi)
this.koi.onUnlock();
@@ -89,6 +96,25 @@ Cards.prototype.toggleBook = function() {
}
};
+Cards.prototype.hideButtons = function() {
+ this.element.removeChild(this.buttonMenu.element);
+
+ if (RUNNING_CAPACITOR)
+ this.element.removeChild(this.buttonLoadCard.element);
+
+ this.buttonsShown = false;
+}
+
+Cards.prototype.showButtons = function() {
+ this.element.appendChild(this.buttonMenu.element);
+
+ if (RUNNING_CAPACITOR)
+ this.element.appendChild(this.buttonLoadCard.element);
+
+ this.buttonsShown = true;
+
+}
+
/**
* Enable the book button
*/
@@ -418,6 +444,7 @@ Cards.prototype.remove = function(card, noChild = false) {
Cards.prototype.hide = function() {
if (this.bookVisible) {
this.book.hide();
+ this.hideButtons();
this.audio.effectBookInteract.play();
this.hideTimer = this.HIDE_TIME;
@@ -431,7 +458,7 @@ Cards.prototype.hide = function() {
Cards.prototype.show = function() {
if (!this.bookVisible) {
this.book.show();
-
+ this.showButtons();
this.bookVisible = true;
this.hidden = false;
}
diff --git a/js/koi/gui/loader/loader.js b/js/koi/gui/loader/loader.js
index 6e4178d4..894c2efd 100644
--- a/js/koi/gui/loader/loader.js
+++ b/js/koi/gui/loader/loader.js
@@ -19,9 +19,10 @@ const Loader = function(
loadFullscreen) {
this.element = element;
this.icon = new LoaderIcon();
+ this.loadInfo = new LoaderLoadInfo();
this.elementSlots = elementSlots;
this.elementButtonSettings = elementButtonSettings;
- this.elementDiscord = new LoaderDiscord();
+ this.elementLinks = new LoaderLinks();
this.resumables = null;
this.outstanding = 0;
this.finished = 0;
@@ -37,13 +38,15 @@ const Loader = function(
this.slots = null;
- element.appendChild(this.elementDiscord.element);
+ element.appendChild(this.elementLinks.element);
if (loadFullscreen)
element.appendChild(this.fullscreen.element);
elementGraphics.appendChild(this.icon.element);
+ elementGraphics.appendChild(this.loadInfo.element);
+
const loop = () => {
this.overlayCanvas.getContext("2d").clearRect(
0,
@@ -85,6 +88,14 @@ Loader.prototype.ID_DISCORD = "loader-discord";
Loader.prototype.BUTTON_DELAY = .37;
Loader.prototype.TRANSITION = StyleUtils.getFloat("--loader-fade-out");
+/**
+ * Set the loading text.
+ * @param {String} text The text to set, or null to use the default text
+ */
+Loader.prototype.setLoadingText = function(text = null) {
+ this.loadInfo.setText(text);
+}
+
/**
* Set the menu
* @param {Menu} menu The menu
@@ -144,16 +155,19 @@ Loader.prototype.complete = function() {
if (this.loadFullscreen)
this.fullscreen.setLoaded();
- const onNewGame = index => {
- this.onNewGame(index);
- this.onFinish();
- this.hide();
+ const onNewGame = (index) => {
+ this.onNewGame(index, () => {
+ this.onFinish();
+ this.hide();
+ });
};
- const onContinue = index => {
- this.onContinue(index);
- this.onFinish();
- this.hide();
+ const onContinue = (index) => {
+ this.onContinue(index, () => {
+ this.onFinish();
+ this.hide();
+
+ });
};
this.slots = [
@@ -162,6 +176,9 @@ Loader.prototype.complete = function() {
new LoaderSlot(2, "3", onNewGame, this.resumables[2] ? onContinue : null)
];
+ // disable loading icon
+ this.loadInfo.hide();
+
for (const slot of this.slots)
this.elementSlots.appendChild(slot.element);
@@ -188,6 +205,11 @@ Loader.prototype.complete = function() {
}
};
+Loader.prototype.finish = function() {
+ this.onFinish();
+ this.hide();
+};
+
/**
* Check whether the loader has finished loading
* @returns {Boolean} True if the loader has already finished
diff --git a/js/koi/gui/loader/loaderAndroid.js b/js/koi/gui/loader/loaderAndroid.js
new file mode 100644
index 00000000..6934ffd1
--- /dev/null
+++ b/js/koi/gui/loader/loaderAndroid.js
@@ -0,0 +1,60 @@
+/**
+ * A link to the website server
+ * @constructor
+ */
+const LoaderAndroid = function () {
+ this.element = this.createElement();
+
+ this.loadSVG();
+};
+
+LoaderAndroid.prototype.ID = "loader-android";
+LoaderAndroid.prototype.CLASS = "loader-bar";
+LoaderAndroid.prototype.CLASS_INVISIBLE = "invisible";
+LoaderAndroid.prototype.FILE = "svg/Google_Play_Store_badge_EN_WHITE.svg";
+LoaderAndroid.prototype.FADE_IN_DELAY = 2.5;
+LoaderAndroid.prototype.URL = "https://play.google.com/store/apps/details?id=com.koifarmgame&utm_source=KoiGame&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1";
+/**
+ * Load the SVG image
+ */
+LoaderAndroid.prototype.loadSVG = function () {
+ const request = new XMLHttpRequest();
+
+ request.onload = () => {
+ this.element.innerHTML = request.responseText;
+
+ this.element.onclick = () => {
+ if (window["require"]) {
+ window["require"]("electron")["shell"]["openExternal"](this.URL);
+ } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openWebsiteHandler) {
+ window.webkit.messageHandlers.openWebsiteHandler.postMessage({discordURL: this.URL});
+ } else {
+ window.open(this.URL, "_blank");
+ }
+ };
+ };
+
+ request.open("GET", this.FILE, true);
+ request.send();
+}
+
+LoaderAndroid.prototype.setInvisible = function () {
+ this.element.classList.add(this.CLASS_INVISIBLE);
+}
+
+LoaderAndroid.prototype.setVisible = function () {
+ this.element.classList.remove(this.CLASS_INVISIBLE);
+}
+
+/**
+ * Create the element
+ * @returns {HTMLDivElement} The element
+ */
+LoaderAndroid.prototype.createElement = function () {
+ const element = document.createElement("div");
+
+ element.id = this.ID;
+ element.classList.add(this.CLASS);
+
+ return element;
+};
diff --git a/js/koi/gui/loader/loaderApple.js b/js/koi/gui/loader/loaderApple.js
new file mode 100644
index 00000000..d78c4e68
--- /dev/null
+++ b/js/koi/gui/loader/loaderApple.js
@@ -0,0 +1,53 @@
+/**
+ * A link to the website server
+ * @constructor
+ */
+const LoaderApple = function() {
+ this.element = this.createElement();
+
+ this.loadSVG();
+};
+
+LoaderApple.prototype.ID = "loader-apple";
+LoaderApple.prototype.CLASS = "loader-bar";
+LoaderApple.prototype.CLASS_INVISIBLE = "invisible";
+LoaderApple.prototype.FILE = "svg/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg";
+LoaderApple.prototype.FADE_IN_DELAY = 2.5;
+LoaderApple.prototype.URL = "https://apps.apple.com/app/koi-farm/id1607489625";
+
+/**
+ * Load the SVG image
+ */
+LoaderApple.prototype.loadSVG = function() {
+ const request = new XMLHttpRequest();
+
+ request.onload = () => {
+ this.element.innerHTML = request.responseText;
+
+ this.element.onclick = () => {
+ if (window["require"]) {
+ window["require"]("electron")["shell"]["openExternal"](this.URL);
+ } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openWebsiteHandler) {
+ window.webkit.messageHandlers.openWebsiteHandler.postMessage({discordURL: this.URL});
+ } else {
+ window.open(this.URL, "_blank");
+ }
+ };
+ };
+
+ request.open("GET", this.FILE, true);
+ request.send();
+}
+
+/**
+ * Create the element
+ * @returns {HTMLDivElement} The element
+ */
+LoaderApple.prototype.createElement = function() {
+ const element = document.createElement("div");
+
+ element.id = this.ID;
+ element.classList.add(this.CLASS);
+
+ return element;
+};
diff --git a/js/koi/gui/loader/loaderDiscord.js b/js/koi/gui/loader/loaderDiscord.js
index b5e7864f..4b159e0d 100644
--- a/js/koi/gui/loader/loaderDiscord.js
+++ b/js/koi/gui/loader/loaderDiscord.js
@@ -9,8 +9,9 @@ const LoaderDiscord = function() {
};
LoaderDiscord.prototype.ID = "loader-discord";
+LoaderDiscord.prototype.CLASS = "loader-icon";
LoaderDiscord.prototype.CLASS_INVISIBLE = "invisible";
-LoaderDiscord.prototype.FILE = "svg/discord.svg";
+LoaderDiscord.prototype.FILE = "svg/discord-mark-white.svg";
LoaderDiscord.prototype.FADE_IN_DELAY = 2.5;
LoaderDiscord.prototype.URL = "https://discord.com/invite/bw3ZFe63Qg";
@@ -51,6 +52,7 @@ LoaderDiscord.prototype.createElement = function() {
element.id = this.ID;
element.className = this.CLASS_INVISIBLE;
+ element.classList.add(this.CLASS);
return element;
};
diff --git a/js/koi/gui/loader/loaderLinks.js b/js/koi/gui/loader/loaderLinks.js
new file mode 100644
index 00000000..f98982f5
--- /dev/null
+++ b/js/koi/gui/loader/loaderLinks.js
@@ -0,0 +1,65 @@
+/**
+ * A link to the website server
+ * @constructor
+ */
+const LoaderLinks = function() {
+ this.element = this.createElement();
+
+ this.elementApple = new LoaderApple();
+ this.elementAndroid = new LoaderAndroid();
+
+ this.elementDiscord = new LoaderDiscord();
+ this.elementWebsite = new LoaderWebsite();
+
+ if (PLATFORM_NAME !== "android" && PLATFORM_NAME !== "ios") {
+ this.element.appendChild(this.elementApple.element);
+ this.element.appendChild(this.elementAndroid.element);
+ }
+
+ this.element.appendChild(this.elementWebsite.element);
+ this.element.appendChild(this.elementDiscord.element);
+};
+
+LoaderLinks.prototype.ID = "loader-links";
+LoaderLinks.prototype.CLASS_INVISIBLE = "invisible";
+LoaderLinks.prototype.FADE_IN_DELAY = 2.5;
+
+/**
+ * Load the SVG image
+ */
+LoaderLinks.prototype.loadSVG = function() {
+ const request = new XMLHttpRequest();
+
+ request.onload = () => {
+ this.element.innerHTML = request.responseText;
+
+ setTimeout(() => {
+ this.element.classList.remove(this.CLASS_INVISIBLE);
+ }, 1000 * this.FADE_IN_DELAY);
+
+ this.element.onclick = () => {
+ if (window["require"]) {
+ window["require"]("electron")["shell"]["openExternal"](this.URL);
+ } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openLinksHandler) {
+ window.webkit.messageHandlers.openLinksHandler.postMessage({discordURL: this.URL});
+ } else {
+ window.open(this.URL, "_blank");
+ }
+ };
+ };
+
+ request.open("GET", this.FILE, true);
+ request.send();
+}
+
+/**
+ * Create the element
+ * @returns {HTMLDivElement} The element
+ */
+LoaderLinks.prototype.createElement = function() {
+ const element = document.createElement("div");
+
+ element.id = this.ID;
+
+ return element;
+};
diff --git a/js/koi/gui/loader/loaderLoadInfo.js b/js/koi/gui/loader/loaderLoadInfo.js
new file mode 100644
index 00000000..149aaafe
--- /dev/null
+++ b/js/koi/gui/loader/loaderLoadInfo.js
@@ -0,0 +1,69 @@
+/**
+ * The loader load info
+ * @constructor
+ */
+const LoaderLoadInfo = function() {
+ this.iconElement = document.createElement("div");
+ this.textElement = document.createElement("div");
+ this.element = this.createElement();
+
+
+ this.loadSVG();
+};
+
+LoaderLoadInfo.prototype.ID = "loader-loading";
+LoaderLoadInfo.prototype.TEXT_ID = "loader-loading-text";
+LoaderLoadInfo.prototype.ICON_ID = "loader-loading-icon";
+LoaderLoadInfo.prototype.TEXT = "LOADING";
+LoaderLoadInfo.prototype.CLASS_INVISIBLE = "invisible";
+LoaderLoadInfo.prototype.FILE = "svg/butterfly.svg";
+LoaderLoadInfo.prototype.FADE_IN_DELAY = .32;
+LoaderLoadInfo.prototype.LOADING_TEXT = "LOADING";
+
+
+/**
+ * Load the SVG image
+ */
+LoaderLoadInfo.prototype.loadSVG = function() {
+ const request = new XMLHttpRequest();
+
+ request.onload = () => {
+ this.iconElement.innerHTML = request.responseText;
+ };
+
+ request.open("GET", LoaderLoadInfo.prototype.FILE, true);
+ request.send();
+};
+
+/**
+ * Set the text
+ * @param {String} text The text, or null to use the default text
+ */
+LoaderLoadInfo.prototype.setText = function(text = null) {
+ if (text)
+ this.textElement.innerText = text;
+ else
+ this.textElement.innerText = language.get(LoaderLoadInfo.prototype.LOADING_TEXT);
+}
+
+/**
+ * Create the element
+ * @returns {HTMLDivElement} The element
+ */
+LoaderLoadInfo.prototype.createElement = function() {
+ const element = document.createElement("div");
+
+ element.id = this.ID;
+
+ this.iconElement.id = this.ICON_ID;
+ this.textElement.id = this.TEXT_ID;
+
+ element.appendChild(this.iconElement);
+ element.appendChild(this.textElement);
+
+ return element;
+};
+
+LoaderLoadInfo.prototype.hide = function() {
+ this.element.classList.add(LoaderLoadInfo.prototype.CLASS_INVISIBLE);
+}
\ No newline at end of file
diff --git a/js/koi/gui/loader/loaderWebsite.js b/js/koi/gui/loader/loaderWebsite.js
new file mode 100644
index 00000000..cf1c5c69
--- /dev/null
+++ b/js/koi/gui/loader/loaderWebsite.js
@@ -0,0 +1,62 @@
+/**
+ * A link to the website server
+ * @constructor
+ */
+const LoaderWebsite = function() {
+ this.element = this.createElement();
+ try {
+ LoaderWebsite.prototype.URL += "&referrer=" + encodeURIComponent(PLATFORM_NAME);
+ } catch (e) {
+ }
+
+ this.loadSVG();
+};
+
+LoaderWebsite.prototype.ID = "loader-website";
+LoaderWebsite.prototype.CLASS = "loader-icon";
+LoaderWebsite.prototype.CLASS_INVISIBLE = "invisible";
+LoaderWebsite.prototype.FILE = "svg/website.svg";
+LoaderWebsite.prototype.FADE_IN_DELAY = 2.5;
+LoaderWebsite.prototype.URL = "https://koifarmgame.com?source=KoiGame";
+
+/**
+ * Load the SVG image
+ */
+LoaderWebsite.prototype.loadSVG = function() {
+ const request = new XMLHttpRequest();
+
+ request.onload = () => {
+ this.element.innerHTML = request.responseText;
+
+ setTimeout(() => {
+ this.element.classList.remove(this.CLASS_INVISIBLE);
+ }, 1000 * this.FADE_IN_DELAY);
+
+ this.element.onclick = () => {
+ if (window["require"]) {
+ window["require"]("electron")["shell"]["openExternal"](this.URL);
+ } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.openWebsiteHandler) {
+ window.webkit.messageHandlers.openWebsiteHandler.postMessage({discordURL: this.URL});
+ } else {
+ window.open(this.URL, "_blank");
+ }
+ };
+ };
+
+ request.open("GET", this.FILE, true);
+ request.send();
+}
+
+/**
+ * Create the element
+ * @returns {HTMLDivElement} The element
+ */
+LoaderWebsite.prototype.createElement = function() {
+ const element = document.createElement("div");
+
+ element.id = this.ID;
+ element.className = this.CLASS_INVISIBLE;
+ element.classList.add(this.CLASS);
+
+ return element;
+};
diff --git a/js/koi/gui/overlay/overlay.js b/js/koi/gui/overlay/overlay.js
index 33308ec3..a494d27f 100644
--- a/js/koi/gui/overlay/overlay.js
+++ b/js/koi/gui/overlay/overlay.js
@@ -10,12 +10,16 @@ const Overlay = function(element) {
this.arrowElement = null;
this.arrowParent = null;
this.textElement = null;
+ this.skipElement = null;
};
+Overlay.prototype.CLASS_HOME = "home-button";
Overlay.prototype.CLASS_POINTER = "pointer";
Overlay.prototype.CLASS_HIGHLIGHT = "overlay-highlight";
Overlay.prototype.CLASS_ARROW = "overlay-arrow";
Overlay.prototype.CLASS_TEXT = "text";
+Overlay.prototype.CLASS_SKIP = "skip-button";
+Overlay.prototype.KEY_SKIP = "TUTORIAL_SKIP";
Overlay.prototype.POINTER_RADIUS =
StyleUtils.getInt("--overlay-pointer-radius") +
StyleUtils.getInt("--overlay-pointer-border");
@@ -30,6 +34,14 @@ Overlay.prototype.render = function() {
}
};
+Overlay.prototype.createHomeElement = function() {
+ const element = document.createElement("div");
+
+ element.className = this.CLASS_HOME;
+
+ return element;
+}
+
/**
* Create a pointer element to indicate where the player should do something
* @returns {HTMLDivElement} The element
@@ -85,6 +97,29 @@ Overlay.prototype.createHighlightElement = function() {
return element;
};
+Overlay.prototype.createSkipElement = function() {
+ const element = document.createElement("button");
+
+ element.className = this.CLASS_SKIP;
+
+ element.appendChild(document.createTextNode(language.get(Overlay.prototype.KEY_SKIP)));
+
+ return element;
+}
+
+Overlay.prototype.createHome = function() {
+ this.homeElement = this.createHomeElement();
+ this.element.appendChild(this.homeElement);
+}
+
+Overlay.prototype.deleteHome = function() {
+ if (this.homeElement) {
+ this.element.removeChild(this.homeElement);
+
+ this.homeElement = null;
+ }
+}
+
/**
* Create a pointer
* @returns {Vector2} The pointer position which can be changed
@@ -174,4 +209,31 @@ Overlay.prototype.removeText = function() {
this.element.removeChild(this.textElement);
this.textElement = null;
}
-};
\ No newline at end of file
+};
+
+Overlay.prototype.createSkip = function(callback) {
+ this.deleteSkip();
+
+ this.skipElement = this.createSkipElement();
+
+ this.skipElement.onclick = () => {
+ callback();
+ }
+
+ this.element.appendChild(this.skipElement);
+}
+
+Overlay.prototype.deleteSkip = function() {
+ if (this.skipElement) {
+ this.element.removeChild(this.skipElement);
+
+ this.skipElement = null;
+ }
+}
+
+Overlay.prototype.clear = function() {
+ this.deletePointer();
+ this.deleteArrow();
+ this.removeText();
+ this.deleteSkip();
+}
\ No newline at end of file
diff --git a/js/koi/tutorial/tutorialBreeding.js b/js/koi/tutorial/tutorialBreeding.js
index e7de458b..c178a640 100644
--- a/js/koi/tutorial/tutorialBreeding.js
+++ b/js/koi/tutorial/tutorialBreeding.js
@@ -11,8 +11,12 @@ const TutorialBreeding = function(storage, overlay) {
this.targetedFish = null;
this.bred = false;
this.mutated = false;
+ this.skip = false;
overlay.setText(language.get(this.LANG_MOVE_FISH));
+ overlay.createSkip(() => {
+ this.skip = true;
+ });
};
TutorialBreeding.prototype = Object.create(Tutorial.prototype);
@@ -110,6 +114,12 @@ TutorialBreeding.prototype.pointToSmallPond = function(koi) {
* @returns {Boolean} True if the tutorial has finished
*/
TutorialBreeding.prototype.update = function(koi) {
+ if (this.skip) {
+ this.overlay.clear();
+
+ return true;
+ }
+
switch (this.phase) {
case this.PHASE_MOVE_FISH:
if (this.targetedFish === null) {
@@ -117,7 +127,7 @@ TutorialBreeding.prototype.update = function(koi) {
this.pointer = this.overlay.createPointer();
}
else {
- if (koi.mover.move) {
+ if (koi.mover.move ) {
this.overlay.deletePointer();
this.pointer = null;
@@ -143,7 +153,7 @@ TutorialBreeding.prototype.update = function(koi) {
break;
case this.PHASE_DROP_FISH:
if (!koi.mover.move) {
- if (koi.constellation.small.fish.length === 0) {
+ if (koi.constellation.small.fish.length === 0 ) {
this.overlay.setText(language.get(this.LANG_TO_POND_1));
this.pointToSmallPond(koi);
@@ -157,7 +167,7 @@ TutorialBreeding.prototype.update = function(koi) {
break;
case this.PHASE_TO_POND_1:
- if (koi.constellation.small.fish.length === 1) {
+ if (koi.constellation.small.fish.length === 1 ) {
this.overlay.setText(language.get(this.LANG_TO_POND_2));
this.overlay.deleteArrow();
diff --git a/js/koi/tutorial/tutorialCards.js b/js/koi/tutorial/tutorialCards.js
index 9dbb3756..9470c3be 100644
--- a/js/koi/tutorial/tutorialCards.js
+++ b/js/koi/tutorial/tutorialCards.js
@@ -13,6 +13,11 @@ const TutorialCards = function(storage, overlay) {
this.unlocked = false;
this.stored = false;
this.highlight1 = this.highlight2 = this.highlight3 = this.highlight4 = null;
+ this.skip = false;
+
+ overlay.createSkip(() => {
+ this.skip = true;
+ });
};
TutorialCards.prototype = Object.create(Tutorial.prototype);
@@ -37,6 +42,7 @@ TutorialCards.prototype.start = function() {
this.phase = this.PHASE_CREATE_CARD;
this.forceMutation = false;
this.handEnabled = true;
+
};
/**
@@ -119,17 +125,16 @@ TutorialCards.prototype.markFinished = function(koi) {
* @returns {Boolean} True if the tutorial has finished
*/
TutorialCards.prototype.update = function(koi) {
+ if (this.skip) {
+ this.overlay.clear();
+
+ koi.gui.cards.enableBookButton(koi.audio);
+
+ return true;
+ }
switch (this.phase) {
case this.PHASE_START:
- if (this.mutations < this.MUTATIONS_REQUIRED)
- this.phase = this.PHASE_WAITING;
- else if (this.mutations === this.MUTATIONS_REQUIRED)
- this.start();
- else {
- koi.gui.cards.enableBookButton(koi.audio);
-
- return true;
- }
+ this.start();
break;
case this.PHASE_CREATE_CARD:
@@ -206,7 +211,7 @@ TutorialCards.prototype.update = function(koi) {
if (!koi.gui.cards.bookVisible || this.unlocked) {
this.markFinished(koi);
- this.overlay.removeText();
+ this.overlay.clear();
return true;
}
diff --git a/js/main.js b/js/main.js
index 5a4de0a3..3efa0d53 100644
--- a/js/main.js
+++ b/js/main.js
@@ -18,6 +18,15 @@ let chosenSlot = -1;
const RUNNING_ON_WEBVIEW_IOS = (window.webkit && window.webkit.messageHandlers) ? true : false;
+const RUNNING_CAPACITOR = typeof Capacitor !== "undefined" && Capacitor.platform !== "web";
+
+const PLATFORM_NAME = RUNNING_CAPACITOR ? Capacitor.getPlatform() : "web";
+
+const RUNNING_MOBILE = RUNNING_CAPACITOR && (Capacitor.isNative || Capacitor.platform === "web");
+
+const preferences = RUNNING_CAPACITOR ? new StoragePreferencesCapacitor() : new StorageLocal();
+
+
/**
* Reload the game into the menu
*/
@@ -126,6 +135,80 @@ const makeLanguage = locale => {
}
};
+function removeStatusBar() {
+ if (!RUNNING_CAPACITOR)
+ return;
+
+ if (Capacitor.isPluginAvailable('StatusBar')) {
+ Capacitor.Plugins.StatusBar.hide();
+ } else {
+ console.error("StatusBar plugin not available");
+ }
+}
+
+function setFullScreen() {
+ if (!RUNNING_CAPACITOR)
+ return;
+
+
+ if (PLATFORM_NAME === "android") {
+ try {
+ AndroidFullScreen.immersiveMode(() => {
+ console.log("System UI visibility set");
+ }, () => {
+ console.error("Failed to set system UI visibility");
+ },
+ AndroidFullScreen.CUTOUT_MODE_NEVER);
+ } catch {
+ console.warn("AndroidFullScreen plugin not available");
+
+ removeStatusBar();
+ }
+
+ }
+}
+
+const keepAwake = async () => {
+ if (!RUNNING_CAPACITOR || !Capacitor.isPluginAvailable('KeepAwake'))
+ return;
+
+ await Capacitor.Plugins.KeepAwake.keepAwake();
+};
+
+function setupPlatform (menu, save) {
+ if (!RUNNING_CAPACITOR)
+ return;
+
+ if (PLATFORM_NAME === "android") {
+ if (Capacitor.isPluginAvailable('App')) {
+ Capacitor.Plugins.App.addListener('appStateChange', (state) => {
+ // Check isActive for app state
+ });
+
+ Capacitor.Plugins.App.addListener('appUrlOpen', (data) => {
+
+ });
+
+ Capacitor.Plugins.App.addListener("backButton", () => {
+ if (chosenSlot !== -1) {
+ save();
+ menu.toggle();
+ }
+ });
+ } else {
+ console.error("Capacitor 'App' plugin not available");
+ }
+
+ keepAwake().then(
+ () => {
+ console.log("Keep awake enabled");
+ }
+ );
+ }
+}
+
+setFullScreen();
+
const paramLang = window["localStorage"].getItem(Menu.prototype.KEY_LANGUAGE) || searchParams.get("lang");
const language = paramLang ? makeLanguage(paramLang) : makeLanguage(navigator.language.substring(0, 2));
const loader = new Loader(
@@ -134,17 +217,29 @@ const loader = new Loader(
document.getElementById("loader-slots"),
document.getElementById("loader-button-settings"),
document.getElementById("wrapper"),
- !RUNNING_ON_WEBVIEW_IOS,
- !RUNNING_ON_WEBVIEW_IOS);
+ !RUNNING_MOBILE && !RUNNING_ON_WEBVIEW_IOS,
+ !RUNNING_MOBILE && !RUNNING_ON_WEBVIEW_IOS);
let imperial = false;
+let menu = null;
+let storage = null;
+
+// Set the loading text to a cached value
+preferences.get(LoaderLoadInfo.prototype.LOADING_TEXT).then((value) => {
+ loader.setLoadingText(value ? value : "Loading");
+});
if (gl &&
gl.getExtension("OES_element_index_uint") &&
(gl.vao = gl.getExtension("OES_vertex_array_object"))) {
+
const audioEngine = new AudioEngine(new Random());
const audio = new AudioBank(audioEngine);
language.load(() => {
+ // Cache the loading text and set it
+ preferences.set(LoaderLoadInfo.prototype.LOADING_TEXT, language.get(LoaderLoadInfo.prototype.LOADING_TEXT));
+ loader.setLoadingText();
+
imperial = language.get("UNIT_LENGTH") === "ft";
const settings = {
@@ -161,7 +256,7 @@ if (gl &&
new CodeViewer(document.getElementById("code"), storage),
audio);
const systems = new Systems(gl, new Random(2893), wrapper.clientWidth, wrapper.clientHeight);
- const menu = new Menu(
+ menu = new Menu(
document.getElementById("menu"),
loader.fullscreen,
chosenLocale,
@@ -201,14 +296,23 @@ if (gl &&
* Save the game state to local storage
*/
const save = () => {
- storage.setBuffer(slot, session.serialize(koi, gui));
+ const promise = storage.setBuffer(slot, session.serialize(koi, gui));
+
+ promise.then(() => {
+ console.log("Game saved");
+ }).catch(() => {
+ console.error("Failed to save game");
+ });
};
+ setupPlatform(menu,() => save());
+
/**
* A function that creates a new game session
* @param {number} index Create a new game at a given slot index
+ * @param {function} onFinish A function to call when the game has been loaded
*/
- const newSession = index => {
+ const newSession = (index, onFinish) => {
chosenSlot = index;
slot = slotNames[index];
session = new Session();
@@ -219,34 +323,57 @@ if (gl &&
koi.free();
koi = session.makeKoi(storage, systems, audio, gui, save, new TutorialBreeding(storage, gui.overlay));
+
+ onFinish();
+
};
/**
* Continue an existing game
* @param {number} index Create a new game at a given slot index
+ * @param {function} onFinish A function to call when the game has been loaded
*/
- const continueGame = index => {
+ const continueGame = (index, onFinish) => {
chosenSlot = index;
slot = slotNames[index];
gui.cards.enableBookButton(audio);
try {
- session.deserialize(storage.getBuffer(slot));
+ const p = storage.getBuffer(slot);
+
+ p.then(buffer => {
+ session.deserialize(buffer);
- koi = session.makeKoi(storage, systems, audio, gui, save);
+ koi = session.makeKoi(storage, systems, audio, gui, save);
+
+ onFinish();
+ }).catch(() => {
+ newSession(index, onFinish);
+ });
} catch (error) {
- newSession(index);
+ newSession(index, onFinish);
console.warn(error);
}
};
- loader.setResumables([
- storage.getBuffer(slotNames[0]) !== null,
- storage.getBuffer(slotNames[1]) !== null,
- storage.getBuffer(slotNames[2]) !== null
- ]);
+ const resumablePromisses = [
+ storage.getBuffer(slotNames[0]),
+ storage.getBuffer(slotNames[1]),
+ storage.getBuffer(slotNames[2])
+ ];
+
+ Promise.all(resumablePromisses).then((values) => {
+ loader.setResumables([
+ values[0] !== null,
+ values[1] !== null,
+ values[2] !== null]);
+ }).catch(
+ (error) => {
+ console.error(error);
+ }
+ );
// Trigger the animation frame loop
lastTime = performance.now();
diff --git a/js/render/blit.js b/js/render/blit.js
index f8fc364d..61a10d9a 100644
--- a/js/render/blit.js
+++ b/js/render/blit.js
@@ -34,7 +34,7 @@ void main() {
Blit.prototype.SHADER_FRAGMENT = `#version 100
uniform sampler2D source;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
gl_FragColor = texture2D(source, iUv);
diff --git a/js/render/blur.js b/js/render/blur.js
index 45e3a8da..5782c4e4 100644
--- a/js/render/blur.js
+++ b/js/render/blur.js
@@ -58,8 +58,8 @@ void main() {
Blur.prototype.SHADER_FRAGMENT = `#version 100
uniform sampler2D source;
-uniform mediump vec2 targetSize;
-uniform mediump vec2 direction;
+uniform highp vec2 targetSize;
+uniform highp vec2 direction;
lowp vec4 get(int delta) {
return texture2D(source, (gl_FragCoord.xy + direction * float(delta)) / targetSize);
diff --git a/js/render/bodies.js b/js/render/bodies.js
index da78d51a..db419efa 100644
--- a/js/render/bodies.js
+++ b/js/render/bodies.js
@@ -57,9 +57,9 @@ void main() {
Bodies.prototype.SHADER_FRAGMENT = `#version 100
uniform sampler2D atlas;
-uniform mediump vec2 shadow;
+uniform highp vec2 shadow;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
gl_FragColor = texture2D(atlas, iUv);
@@ -72,7 +72,7 @@ uniform vec2 scale;
attribute vec2 position;
attribute vec2 uv;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
iUv = uv;
@@ -84,7 +84,7 @@ void main() {
Bodies.prototype.SHADER_SHADOWS_FRAGMENT = `#version 100
uniform sampler2D atlas;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
#define TRANSPARENCY 0.6
@@ -128,11 +128,12 @@ Bodies.prototype.render = function(
this.buffer.render();
}
else {
- this.gl.blendFuncSeparate(
- this.gl.SRC_ALPHA,
- this.gl.ONE_MINUS_SRC_ALPHA,
- this.gl.ONE_MINUS_DST_ALPHA,
- this.gl.ONE);
+ // Removing this seems to fix flickering of shadows on older android phones
+ // this.gl.blendFuncSeparate(
+ // this.gl.SRC_ALPHA,
+ // this.gl.ONE_MINUS_SRC_ALPHA,
+ // this.gl.ONE_MINUS_DST_ALPHA,
+ // this.gl.ONE);
this.program.use();
diff --git a/js/render/commonShaders.js b/js/render/commonShaders.js
index 0dddc483..ddfa9ab0 100644
--- a/js/render/commonShaders.js
+++ b/js/render/commonShaders.js
@@ -7,19 +7,19 @@ const CommonShaders = {};
CommonShaders.random = `
uniform sampler2D noise;
-mediump float random2(mediump vec2 x) {
+highp float random2(highp vec2 x) {
return texture2D(noise, x * 0.011).r;
}
-mediump float random3(mediump vec3 x) {
+highp float random3(highp vec3 x) {
return texture2D(noise, x.xy * 0.011 - x.z * 0.017).r;
}
`;
// Cubic interpolation
CommonShaders.cubicInterpolation = `
-mediump float interpolate(mediump float a, mediump float b, mediump float c, mediump float d, mediump float x) {
- mediump float p = (d - c) - (a - b);
+highp float interpolate(highp float a, highp float b, highp float c, highp float d, highp float x) {
+ highp float p = (d - c) - (a - b);
return x * (x * (x * p + ((a - b) - p)) + (c - a)) + b;
}
@@ -27,7 +27,7 @@ mediump float interpolate(mediump float a, mediump float b, mediump float c, med
// 2D cubic noise
CommonShaders.cubicNoise2 = CommonShaders.random + CommonShaders.cubicInterpolation + `
-mediump float sampleX(highp vec2 at) {
+highp float sampleX(highp vec2 at) {
highp float floored = floor(at.x);
return interpolate(
@@ -38,7 +38,7 @@ mediump float sampleX(highp vec2 at) {
at.x - floored) * 0.5 + 0.25;
}
-mediump float cubicNoise(highp vec2 at) {
+highp float cubicNoise(highp vec2 at) {
highp float floored = floor(at.y);
return interpolate(
@@ -52,7 +52,7 @@ mediump float cubicNoise(highp vec2 at) {
// 3D cubic noise
CommonShaders.cubicNoise3 = CommonShaders.random + CommonShaders.cubicInterpolation + `
-mediump float sampleX(highp vec3 at) {
+highp float sampleX(highp vec3 at) {
highp float floored = floor(at.x);
return interpolate(
@@ -63,7 +63,7 @@ mediump float sampleX(highp vec3 at) {
at.x - floored) * 0.5 + 0.25;
}
-mediump float sampleY(highp vec3 at) {
+highp float sampleY(highp vec3 at) {
highp float floored = floor(at.y);
return interpolate(
@@ -74,7 +74,7 @@ mediump float sampleY(highp vec3 at) {
at.y - floored);
}
-mediump float cubicNoise(highp vec3 at) {
+highp float cubicNoise(highp vec3 at) {
highp float floored = floor(at.z);
return interpolate(
diff --git a/js/render/distanceField.js b/js/render/distanceField.js
index 81148c40..ab7234f1 100644
--- a/js/render/distanceField.js
+++ b/js/render/distanceField.js
@@ -31,15 +31,15 @@ void main() {
DistanceField.prototype.SHADER_FRAGMENT = `#version 100
uniform sampler2D source;
-uniform mediump vec2 size;
-uniform mediump float range;
+uniform highp vec2 size;
+uniform highp float range;
-mediump float get(int dx, int dy) {
+highp float get(int dx, int dy) {
return texture2D(source, (gl_FragCoord.xy + vec2(float(dx), float(dy))) / size).a;
}
void main() {
- mediump float nearest = min(
+ highp float nearest = min(
min(
min(
get(-1, -1),
diff --git a/js/render/drops.js b/js/render/drops.js
index d674a6e2..d05b544c 100644
--- a/js/render/drops.js
+++ b/js/render/drops.js
@@ -30,7 +30,7 @@ attribute float threshold;
varying float iAlpha;
void main() {
- mediump float age = mod(window - threshold, 1.0) / windowWidth;
+ highp float age = mod(window - threshold, 1.0) / windowWidth;
iAlpha = transparency * alpha * age;
diff --git a/js/render/fishBackground.js b/js/render/fishBackground.js
index 055bc787..66e0301f 100644
--- a/js/render/fishBackground.js
+++ b/js/render/fishBackground.js
@@ -49,12 +49,12 @@ void main() {
FishBackground.prototype.SHADER_FRAGMENT = `#version 100
uniform lowp vec3 colorInner;
uniform lowp vec3 colorOuter;
-uniform mediump float gradientPower;
+uniform highp float gradientPower;
-varying mediump vec2 iPosition;
+varying highp vec2 iPosition;
void main() {
- mediump float dist = pow(min(1.0, length(iPosition) / 0.707106), gradientPower);
+ highp float dist = pow(min(1.0, length(iPosition) / 0.707106), gradientPower);
gl_FragColor = vec4(mix(colorInner, colorOuter, dist), 1.0);
}
diff --git a/js/render/ponds.js b/js/render/ponds.js
index b2e35e2c..443b3c2b 100644
--- a/js/render/ponds.js
+++ b/js/render/ponds.js
@@ -105,7 +105,7 @@ varying lowp vec2 iUv;
#define WAVE_BASE 0.3
#define WATER_HEIGHT 3.7
-lowp float get(mediump vec2 delta) {
+lowp float get(highp vec2 delta) {
lowp vec2 uv = iUv + delta / waterSize;
lowp vec2 sample = texture2D(water, uv).gr;
diff --git a/js/render/preview.js b/js/render/preview.js
index 22815dbe..487fb568 100644
--- a/js/render/preview.js
+++ b/js/render/preview.js
@@ -11,8 +11,9 @@ const Preview = function(gl, fishBackground) {
};
Preview.prototype = Object.create(ImageMaker.prototype);
-Preview.prototype.PREVIEW_WIDTH = StyleUtils.getInt("--card-preview-width");
-Preview.prototype.PREVIEW_HEIGHT = StyleUtils.getInt("--card-preview-height");
+Preview.prototype.PREVIEW_SCALE = StyleUtils.getFloat("--card-scale");
+Preview.prototype.PREVIEW_WIDTH = StyleUtils.getInt("--card-preview-width-default") * Preview.prototype.PREVIEW_SCALE;
+Preview.prototype.PREVIEW_HEIGHT = StyleUtils.getInt("--card-preview-height-default") * Preview.prototype.PREVIEW_SCALE;
Preview.prototype.PREVIEW_COLUMNS = StyleUtils.getInt("--card-preview-columns");
Preview.prototype.PREVIEW_ROWS = StyleUtils.getInt("--card-preview-rows");
Preview.prototype.SCALE = 150;
@@ -33,6 +34,12 @@ Preview.prototype.render = function(body, atlas, bodies) {
this.target.target();
this.gl.enable(this.gl.SCISSOR_TEST);
+ this.gl.blendFuncSeparate(
+ this.gl.SRC_ALPHA,
+ this.gl.ONE_MINUS_SRC_ALPHA,
+ this.gl.ONE_MINUS_DST_ALPHA,
+ this.gl.ONE);
+
for (let row = 0; row < this.PREVIEW_ROWS; ++row) for (let column = 0; column < this.PREVIEW_COLUMNS; ++column) {
const left = column * this.PREVIEW_WIDTH;
const top = (this.PREVIEW_ROWS - row) * this.PREVIEW_HEIGHT;
@@ -60,6 +67,9 @@ Preview.prototype.render = function(body, atlas, bodies) {
bodies.render(atlas, widthMeters, -heightMeters, false);
}
+ this.gl.blendFunc(
+ this.gl.SRC_ALPHA,
+ this.gl.ONE_MINUS_SRC_ALPHA);
this.gl.disable(this.gl.SCISSOR_TEST);
return this.toCanvas();
diff --git a/js/render/sand.js b/js/render/sand.js
index 08a2ea73..e82f74dd 100644
--- a/js/render/sand.js
+++ b/js/render/sand.js
@@ -51,11 +51,11 @@ void main() {
Sand.prototype.SHADER_FRAGMENT = `#version 100
` + CommonShaders.cubicNoise2 + `
-uniform mediump float scale;
+uniform highp float scale;
uniform lowp vec3 colorDeep;
uniform lowp vec3 colorShallow;
-varying mediump vec2 iDepth;
+varying highp vec2 iDepth;
void main() {
lowp float noise = pow(random2(gl_FragCoord.xy), 9.0);
diff --git a/js/render/shadows.js b/js/render/shadows.js
index 077a28a2..14a7c6b7 100644
--- a/js/render/shadows.js
+++ b/js/render/shadows.js
@@ -49,15 +49,15 @@ void main() {
Shadows.prototype.FRAGMENT_SHADER = `#version 100
uniform sampler2D source;
-uniform mediump float meter;
-uniform mediump float shadowDepth;
+uniform highp float meter;
+uniform highp float shadowDepth;
-varying mediump vec2 iDepth;
-varying mediump vec2 iUv;
+varying highp vec2 iDepth;
+varying highp vec2 iUv;
void main() {
- mediump float depthFactor = iDepth.y * (0.5 - 0.5 * cos(3.141592 * sqrt(iDepth.x)));
- mediump float depth = depthFactor * meter * shadowDepth;
+ highp float depthFactor = iDepth.y * (0.5 - 0.5 * cos(3.141592 * sqrt(iDepth.x)));
+ highp float depth = depthFactor * meter * shadowDepth;
gl_FragColor = texture2D(source, iUv + vec2(depth * 0.5, depth));
}
diff --git a/js/render/still.js b/js/render/still.js
index 96884715..4cb13262 100644
--- a/js/render/still.js
+++ b/js/render/still.js
@@ -31,6 +31,17 @@ Still.prototype.render = function(body, atlas, bodies) {
this.fishBackground.render(widthMeters, heightMeters);
+ // this.gl.blendFunc(
+ // this.gl.SRC_ALPHA,
+ // this.gl.ONE_MINUS_SRC_ALPHA);
+
+ this.gl.blendFuncSeparate(
+ this.gl.SRC_ALPHA,
+ this.gl.ONE_MINUS_SRC_ALPHA,
+ this.gl.ONE_MINUS_DST_ALPHA,
+ this.gl.ONE);
+
+
body.renderLoop(
(this.RADIUS + Math.cos(this.ANGLE) * this.RADIUS) * this.UPSCALE / this.SCALE,
(this.RADIUS + Math.sin(this.ANGLE) * this.RADIUS) * this.UPSCALE / this.SCALE,
diff --git a/js/render/waves.js b/js/render/waves.js
index c5c7a469..70a55621 100644
--- a/js/render/waves.js
+++ b/js/render/waves.js
@@ -44,20 +44,20 @@ void main() {
Waves.prototype.SHADER_FRAGMENT = `#version 100
uniform sampler2D source;
-uniform mediump vec2 size;
-uniform mediump float damping;
+uniform highp vec2 size;
+uniform highp float damping;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
- mediump vec2 step = 1.0 / size;
- mediump vec3 state = texture2D(source, iUv).rgb;
- mediump float hLeft = texture2D(source, vec2(iUv.x - step.x, iUv.y)).r;
- mediump float hRight = texture2D(source, vec2(iUv.x + step.x, iUv.y)).r;
- mediump float hUp = texture2D(source, vec2(iUv.x, iUv.y - step.y)).r;
- mediump float hDown = texture2D(source, vec2(iUv.x, iUv.y + step.y)).r;
- mediump float momentum = (state.g + state.b) * 2.0 - 1.0;
- mediump float newHeight = (hLeft + hUp + hRight + hDown) - 2.0;
+ highp vec2 step = 1.0 / size;
+ highp vec3 state = texture2D(source, iUv).rgb;
+ highp float hLeft = texture2D(source, vec2(iUv.x - step.x, iUv.y)).r;
+ highp float hRight = texture2D(source, vec2(iUv.x + step.x, iUv.y)).r;
+ highp float hUp = texture2D(source, vec2(iUv.x, iUv.y - step.y)).r;
+ highp float hDown = texture2D(source, vec2(iUv.x, iUv.y + step.y)).r;
+ highp float momentum = (state.g + state.b) * 2.0 - 1.0;
+ highp float newHeight = (hLeft + hUp + hRight + hDown) - 2.0;
gl_FragColor = vec4(
((newHeight - momentum) * damping) * 0.5 + 0.5,
diff --git a/js/render/wind.js b/js/render/wind.js
index 64c03faf..f5522c93 100644
--- a/js/render/wind.js
+++ b/js/render/wind.js
@@ -31,7 +31,7 @@ Wind.prototype.DAMPING = .98;
Wind.prototype.SHADER_VERTEX = `#version 100
attribute vec2 position;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
iUv = position * 0.5 + 0.5;
@@ -43,17 +43,17 @@ void main() {
Wind.prototype.SHADER_FRAGMENT = `#version 100
uniform sampler2D source;
uniform sampler2D springs;
-uniform mediump float damping;
+uniform highp float damping;
-varying mediump vec2 iUv;
+varying highp vec2 iUv;
void main() {
lowp vec3 previous = texture2D(source, iUv).rgb;
- mediump float previousState = previous.r * 2.0 - 1.0;
- mediump float previousLeft = previous.g;
- mediump float previousRight = previous.b;
- mediump float motion = previousRight - previousLeft;
- mediump float state = previousState + motion * 0.4;
+ highp float previousState = previous.r * 2.0 - 1.0;
+ highp float previousLeft = previous.g;
+ highp float previousRight = previous.b;
+ highp float motion = previousRight - previousLeft;
+ highp float state = previousState + motion * 0.4;
motion = (motion - state * texture2D(springs, iUv).r) * damping;
diff --git a/js/storage/storageFile.js b/js/storage/storageFile.js
index 860a09dc..03e3a432 100644
--- a/js/storage/storageFile.js
+++ b/js/storage/storageFile.js
@@ -34,19 +34,19 @@ if (window["require"]) {
"extensions": ["png"]
};
- StorageFile.prototype.set = function (key, value) {
+ StorageFile.prototype.set = async function (key, value) {
makeDirectory();
fs["writeFileSync"](url["pathToFileURL"](directory + key + this.EXTENSION), value);
};
- StorageFile.prototype.setBuffer = function(key, value) {
+ StorageFile.prototype.setBuffer = async function(key, value) {
makeDirectory();
fs["writeFileSync"](url["pathToFileURL"](directory + key + this.EXTENSION), value.toByteArray());
};
- StorageFile.prototype.get = function(key) {
+ StorageFile.prototype.get = async function(key) {
const file = directory + key + this.EXTENSION;
let contents = null;
@@ -61,7 +61,7 @@ if (window["require"]) {
return contents;
};
- StorageFile.prototype.getBuffer = function(key) {
+ StorageFile.prototype.getBuffer = async function(key) {
const file = directory + key + this.EXTENSION;
let contents = null;
@@ -76,7 +76,7 @@ if (window["require"]) {
return contents ? new BinBuffer(contents) : null;
};
- StorageFile.prototype.remove = function(key) {
+ StorageFile.prototype.remove = async function(key) {
const file = directory + key + this.EXTENSION;
if (fileExists(file))
diff --git a/js/storage/storageFileCapacitor.js b/js/storage/storageFileCapacitor.js
new file mode 100644
index 00000000..1a0732cb
--- /dev/null
+++ b/js/storage/storageFileCapacitor.js
@@ -0,0 +1,196 @@
+/**
+ * A storage system using files
+ * @constructor
+ */
+const StorageFileCapacitor = function() {
+ StorageSystem.call(this);
+
+ this.hasClipboard = false;
+
+ if (!Capacitor.isPluginAvailable('Filesystem')) {
+ throw new Error('Capacitor Preferences API is not available');
+ }
+
+};
+
+StorageFileCapacitor.prototype = Object.create(StorageSystem.prototype);
+StorageFileCapacitor.prototype.EXTENSION = ".sav";
+StorageFileCapacitor.prototype.DIRECTORY_SAVE = "save/";
+StorageFileCapacitor.prototype.DIRECTORY_IMAGE = "koi/";
+
+const directoryLibrary = "LIBRARY";
+const directoryData = "DOCUMENTS";
+const encodingText = "utf8";
+
+const writeFileCap = async (filename, content, dir, encoding=null) => {
+ await Capacitor.Plugins.Filesystem.writeFile({
+ path: filename,
+ data: content,
+ directory: dir,
+ encoding: encoding,
+ });
+};
+
+const readFileCap = async (filename, dir, encoding=null) => {
+ const contents = await Capacitor.Plugins.Filesystem.readFile({
+ path: filename,
+ directory: dir,
+ encoding: encoding,
+ });
+
+ return contents.data;
+};
+
+const deleteFileCap = async (filename, dir = directoryLibrary) => {
+ await Capacitor.Plugins.Filesystem.deleteFile({
+ path: filename,
+ directory: dir,
+ });
+};
+
+const fileExistsCap = async (filename, dir = directoryLibrary) => {
+ try {
+ await Capacitor.Plugins.Filesystem.stat(
+ {
+ path: filename,
+ directory: dir,
+ }
+ );
+ return true;
+ } catch (checkDirException) {
+ if (checkDirException.message === 'File does not exist') {
+ return false;
+ } else {
+ return false;
+ }
+ }
+};
+
+const makeDirectory = async (path, dir = directory) => {
+ if (await fileExistsCap(path, dir))
+ return;
+
+ try {
+ await Capacitor.Plugins.Filesystem.mkdir({
+ path: path,
+ directory: dir,
+ recursive: true
+ });
+ } catch (error) {
+ if (error.message.toLowerCase() !== "directory exists")
+ throw error;
+ }
+
+};
+
+const fileExists = name => {
+ return fileExistsCap(name);
+};
+
+const pngFilter = {
+ "name": "PNG Image",
+ "extensions": ["png"]
+};
+
+StorageFileCapacitor.prototype.set = async function (key, value) {
+ return writeFileCap(key + this.EXTENSION, value, directoryLibrary, encodingText);
+};
+
+StorageFileCapacitor.prototype.setBuffer = async function(key, value) {
+ // makeDirectory();
+
+ return writeFileCap(key + this.EXTENSION, value.toString(), directoryLibrary, encodingText);
+};
+
+StorageFileCapacitor.prototype.get = async function(key) {
+ const file = key + this.EXTENSION;
+ let contents = null;
+
+ try {
+ if (await fileExists(file))
+ contents = readFileCap(file, directoryLibrary, encodingText);
+ }
+ catch (error) {
+
+ }
+
+ return contents;
+};
+
+StorageFileCapacitor.prototype.getBuffer = async function(key) {
+ const file = key + this.EXTENSION;
+ let contents = null;
+
+ try {
+ if (await fileExists(file))
+ contents = await readFileCap(file, directoryLibrary, encodingText);
+ }
+ catch (error) {
+
+ }
+
+ return contents ? new BinBuffer(contents) : null;
+};
+
+StorageFileCapacitor.prototype.remove = async function(key) {
+ const file = key + this.EXTENSION;
+
+ if (await fileExists(file))
+ await deleteFileCap(file);
+};
+
+const pickFile = async () => {
+ const result = await Capacitor.Plugins.FilePicker.pickImages({
+ "limit": 0,
+ "readData": true
+ });
+ return result.files;
+};
+
+const pickFiles = async () => {
+ const result = await Capacitor.Plugins.FilePicker.pickFiles({
+ "limit": 0,
+ "extensions": ["png", "jpg", "jpeg"],
+ "readData": true
+ });
+ return result.files;
+}
+
+StorageFileCapacitor.prototype.loadImage = async function(name) {
+ const files = await pickFile();
+
+ const e = new CustomEvent('loadImage');
+
+ e.dataTransfer = {
+ files: files
+ }
+
+ window.dispatchEvent(e);
+}
+
+StorageFileCapacitor.prototype.imageToFile = async function(blob, name) {
+
+ makeDirectory(this.DIRECTORY_IMAGE, directoryData).then( () => {
+ const file = this.DIRECTORY_IMAGE + name;
+
+ const reader = new FileReader();
+
+ reader.onloadend = function() {
+ const base64Data = reader.result;
+
+ writeFileCap(file, base64Data, directoryData).then( () => {
+ if (Capacitor.isPluginAvailable('Toast'))
+ Capacitor.Plugins.Toast.show({
+ text: 'File written',
+ duration: 'long'
+ });
+ });
+ }
+
+ reader.readAsDataURL(blob);
+
+ }
+ );
+
+
+};
diff --git a/js/storage/storageLocal.js b/js/storage/storageLocal.js
index d7676e79..02cafe3f 100644
--- a/js/storage/storageLocal.js
+++ b/js/storage/storageLocal.js
@@ -8,13 +8,36 @@ const StorageLocal = function() {
StorageLocal.prototype = Object.create(StorageSystem.prototype);
+StorageLocal.prototype.setItem = async function(key, value) {
+ return new Promise((resolve, reject) => {
+ resolve(window.localStorage.setItem(key, value));
+ }
+ );
+};
+
+StorageLocal.prototype.getItem = async function(key) {
+ return new Promise((resolve, reject) => {
+ if (window.localStorage.getItem(key) === null)
+ resolve(null);
+ else
+ resolve(window.localStorage.getItem(key));
+ });
+};
+
+StorageLocal.prototype.removeItem = async function(key){
+ return new Promise((resolve, reject) => {
+ resolve(window.localStorage.removeItem(key));
+ }
+ );
+};
+
/**
* Set the value of an item
* @param {String} key The key of the item
* @param {String} value The value of the item
*/
StorageLocal.prototype.set = function(key, value) {
- window["localStorage"].setItem(key, value);
+ return this.setItem(key, value);
};
/**
@@ -23,7 +46,7 @@ StorageLocal.prototype.set = function(key, value) {
* @param {BinBuffer} value The buffer of the item
*/
StorageLocal.prototype.setBuffer = function(key, value) {
- this.set(key, value.toString());
+ return this.set(key, value.toString());
};
/**
@@ -32,7 +55,8 @@ StorageLocal.prototype.setBuffer = function(key, value) {
* @returns {String|null} The value of the item, or null if it does not exist
*/
StorageLocal.prototype.get = function(key) {
- return window["localStorage"].getItem(key);
+ const item = this.getItem(key);
+ return item;
};
/**
@@ -40,8 +64,8 @@ StorageLocal.prototype.get = function(key) {
* @param {String} key The key of the buffer
* @returns {BinBuffer|null} The buffer, or null if it does not exist
*/
-StorageLocal.prototype.getBuffer = function(key) {
- const string = this.get(key);
+StorageLocal.prototype.getBuffer = async function(key) {
+ const string = await this.get(key);
if (string)
return new BinBuffer(string);
@@ -54,7 +78,7 @@ StorageLocal.prototype.getBuffer = function(key) {
* @param {String} key The key of the item
*/
StorageLocal.prototype.remove = function(key) {
- window["localStorage"].removeItem(key);
+ this.removeItem(key);
};
/**
diff --git a/js/storage/storagePreferencesCapacitor.js b/js/storage/storagePreferencesCapacitor.js
new file mode 100644
index 00000000..9168287d
--- /dev/null
+++ b/js/storage/storagePreferencesCapacitor.js
@@ -0,0 +1,103 @@
+/**
+ * A storage system using the browsers local storage
+ * @constructor
+ */
+const StoragePreferencesCapacitor = function() {
+ StorageSystem.call(this);
+
+ if (!Capacitor.isPluginAvailable('Preferences')) {
+ throw new Error('Capacitor Preferences API is not available');
+ }
+};
+
+StoragePreferencesCapacitor.prototype = Object.create(StorageSystem.prototype);
+
+StoragePreferencesCapacitor.prototype.setItem = async function(key, value) {
+ return await Capacitor.Plugins.Preferences.set({
+ key: key,
+ value: JSON.stringify(value),
+ });
+};
+
+StoragePreferencesCapacitor.prototype.getItem = async function(key) {
+ const item = await Capacitor.Plugins.Preferences.get({ key: key });
+ return JSON.parse(item.value);
+};
+
+StoragePreferencesCapacitor.prototype.removeItem = async function(key){
+ return await Capacitor.Plugins.Preferences.remove({
+ key: key,
+ });
+};
+
+/**
+ * Set the value of an item
+ * @param {String} key The key of the item
+ * @param {String} value The value of the item
+ */
+StoragePreferencesCapacitor.prototype.set = function(key, value) {
+ return this.setItem(key, value);
+};
+
+/**
+ * Set the buffer of an item
+ * @param {String} key The key of the item
+ * @param {BinBuffer} value The buffer of the item
+ */
+StoragePreferencesCapacitor.prototype.setBuffer = function(key, value) {
+ return this.set(key, value.toString());
+};
+
+/**
+ * Get an item
+ * @param {String} key The key of the item
+ * @returns {String|null} The value of the item, or null if it does not exist
+ */
+StoragePreferencesCapacitor.prototype.get = function(key) {
+ const item = this.getItem(key);
+ return item;
+};
+
+/**
+ * Get a buffer
+ * @param {String} key The key of the buffer
+ * @returns {BinBuffer|null} The buffer, or null if it does not exist
+ */
+StoragePreferencesCapacitor.prototype.getBuffer = async function(key) {
+ const string = await this.get(key);
+
+ if (string)
+ return new BinBuffer(string);
+
+ return null;
+};
+
+/**
+ * Remove an item
+ * @param {String} key The key of the item
+ */
+StoragePreferencesCapacitor.prototype.remove = function(key) {
+ this.removeItem(key);
+};
+
+/**
+ * Save an image
+ * @param {Blob} blob The image blob data
+ * @param {String} name The file name
+ */
+StoragePreferencesCapacitor.prototype.imageToFile = function(blob, name) {
+ const a = document.createElement("a");
+ const url = URL.createObjectURL(blob);
+
+ a.href = url;
+ a.download = name;
+
+ document.body.appendChild(a);
+
+ a.click();
+
+ setTimeout(() => {
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ }, 0);
+};
\ No newline at end of file
diff --git a/svg/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg b/svg/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg
new file mode 100644
index 00000000..072b425a
--- /dev/null
+++ b/svg/Download_on_the_App_Store_Badge_US-UK_RGB_blk_092917.svg
@@ -0,0 +1,46 @@
+
diff --git a/svg/Google_Play_Store_badge_EN_WHITE.svg b/svg/Google_Play_Store_badge_EN_WHITE.svg
new file mode 100644
index 00000000..cd64c0ea
--- /dev/null
+++ b/svg/Google_Play_Store_badge_EN_WHITE.svg
@@ -0,0 +1,13 @@
+
+
+
+
diff --git a/svg/add.svg b/svg/add.svg
new file mode 100644
index 00000000..65ea2875
--- /dev/null
+++ b/svg/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/butterfly.svg b/svg/butterfly.svg
new file mode 100644
index 00000000..e73575b2
--- /dev/null
+++ b/svg/butterfly.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/svg/discord-mark-white.svg b/svg/discord-mark-white.svg
new file mode 100644
index 00000000..7f9a31f0
--- /dev/null
+++ b/svg/discord-mark-white.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/settings.svg b/svg/settings.svg
new file mode 100644
index 00000000..f4866a11
--- /dev/null
+++ b/svg/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/svg/website.svg b/svg/website.svg
new file mode 100644
index 00000000..8b96a41d
--- /dev/null
+++ b/svg/website.svg
@@ -0,0 +1 @@
+
\ No newline at end of file