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 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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