feat(server): finish server part (1/2)

This commit is contained in:
Halyul
2021-05-31 19:53:11 -04:00
parent 7d7e10035e
commit bec0564f20
29 changed files with 515 additions and 590100 deletions

View File

@@ -6,14 +6,13 @@ The `LICENSE` file applies to all files unless listed specifically.
`LICENSE_SPINE` file applies to following files including adapted code for this repo:
- `template/build/spine-widget.js`
- `template/new/build/spine-player.css`
- `template/new/build/spine-player.js`
- `release/*/build/spine-player.css`
- `release/*/build/spine-player.js`
- `template/assets/spine-player.css`
- `template/assets/spine-player.js`
- `release/*/assets/spine-player.css`
- `release/*/assets/spine-player.js`
`Copyright © 2017 - 2021 Arknights/Hypergryph Co., Ltd` applies to following files:
- all files under `operator` folder and its sub-folders
- all files under `release/*/assets/*` folder
- `release/*/build/operator.js`
- all files under `release/*/operator/*` folder
- `release/*/operator/operator.js`

95
aklive2d.py Normal file
View File

@@ -0,0 +1,95 @@
import argparse
import sys
from lib.config import Config
from lib.server import Server
from lib.builder import Builder
class AkLive2D:
def __init__(self) -> None:
self.config = Config().config
self.args = None
self.running = None
def start(self):
parser = argparse.ArgumentParser(
prog="aklive2d",
description="Arknights Live 2D Wallpaper Builder",
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument(
"-d",
"--debug",
dest="debug",
action="store_true",
help="To debug the app"
)
subprasers = parser.add_subparsers(
title="Available commands",
dest="command",
required=True,
help="<Required> Select the command to run"
)
server = subprasers.add_parser(
"server",
help="Development Server",
aliases=['s'],
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
server.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8080,
help="Development server port"
)
server.add_argument(
"-o",
"--operator",
dest="operator_name",
type=str,
required=True,
help="<Required> Operatro to develop"
)
build = subprasers.add_parser(
"build",
help="Build releases",
aliases=['b'],
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
build.add_argument(
"-o",
"--operator",
dest="operator_names",
type=str,
default="all",
nargs='+',
help="Operatro to build",
choices=["all"] + [key for key in self.config["operators"]]
)
self.args = parser.parse_args()
if self.args.command == "server" or self.args.command == "s":
self.running = Server(self.args.port, self.args.operator_name, self.config)
if self.args.command == "build" or self.args.command == "b":
self.running = Builder(self.args.operator_names, self.config["operators"])
self.running.start()
def stop(self):
self.running.stop()
return
if __name__ == "__main__":
aklive2d = AkLive2D()
try:
aklive2d.start()
except KeyboardInterrupt:
print("\nInterrupted, exiting...")
sys.exit()

View File

@@ -1,12 +1,10 @@
operators:
skadi:
common_name: dyn_illust_char_1012_skadi2
source_folder: ./operator/skadi/extracted/
target_folder: ./operator/skadi/
release_folder: ./release/{name}/
source_folder: ./operator/{name}/extracted/
target_folder: ./operator/{name}/
server:
asset_access_folder: /asset/
css_access_folder: /build/
js_access_folder: /build/
operator_folder: /operator/
release_folder: ./release/
template_folder: ./template/
version_file: ./VERSION
template_folder: /template/

44
lib/base64_util.py Normal file
View File

@@ -0,0 +1,44 @@
import base64
import pathlib
def encode_image(path, prefix=True):
with open(pathlib.Path.cwd().joinpath(path), "rb") as f:
bytes = f.read()
encoded_bytes = base64.b64encode(bytes)
humanreadable_data = encoded_bytes.decode("utf-8")
if prefix is True:
result = "data:image/png;base64," + humanreadable_data
else:
result = humanreadable_data
return result
def decode_image(data: str, path):
if data.strip().startswith("data:") is True:
data = data.split(",")[1]
encoded_bytes = data.encode("utf-8")
with open(pathlib.Path.cwd().joinpath(path), "wb") as f:
bytes = base64.decodebytes(encoded_bytes)
f.write(bytes)
def encode_string(data=None, type="text/plain", path=None, prefix=True, encoding="utf-8"):
if data is None and path is None:
return
if data is not None:
bytes = data.encode(encoding)
elif path is not None:
with open(pathlib.Path.cwd().joinpath(path), "r") as f:
bytes = f.read()
encoded_bytes = base64.b64encode(bytes)
humanreadable_data = encoded_bytes.decode(encoding)
if prefix is True:
result = "data:{};base64,".format(type) + humanreadable_data
else:
result = humanreadable_data
return result
def decode_string(data:str, encoding="utf-8"):
if data.strip().startswith("data:") is True:
data = data.split(",")[1]
encoded_bytes = data.encode(encoding)
bytes = base64.decodebytes(encoded_bytes)
return bytes.decode(encoding)

View File

@@ -1,46 +1,153 @@
import threading
import shutil
from lib.skeleton_binary import SkeletonBinary
from lib.alpha_composite import AlphaComposite
from lib.atlas_reader import AtlasReader
from lib.base64_util import *
class Builder:
def __init__(self, operator_name, config) -> None:
self.operator_name = operator_name
self.config = config
self.source_path = config["operators"][operator_name]["source_folder"].format(name=operator_name)
self.target_path = config["operators"][operator_name]["target_folder"].format(name=operator_name)
self.common_name = config["operators"][operator_name]["common_name"]
self.file_paths = dict(
json=self.target_path + self.common_name + ".json",
atlas=self.target_path + self.common_name + ".atlas",
skel=self.target_path + self.common_name + ".skel",
)
def start(self):
self.__set_version()
# self.__build_assets()
# self.__set_version()
# name = "skadi"
# print(self.config[name]["source_folder"].format(name=name))
source = "./operator/skadi/extracted/"
target = "./operator/skadi/"
name = "dyn_illust_char_1012_skadi2"
print("build")
return
def stop(self):
return
def __build_assets(self):
source = "./operator/skadi/extracted/"
target = "./operator/skadi/"
name = "dyn_illust_char_1012_skadi2"
ar = AtlasReader(source + name, target + name)
skeleton_binary = threading.Thread(
def build_assets(self):
operator_file = pathlib.Path.cwd().joinpath(self.target_path, "operator.js")
if operator_file.exists() is False:
print("Building operaotr data...")
alpha_composite_threads = list()
png_to_base64_threads = list()
prefix = "window.operator = "
data = dict()
ar = AtlasReader(self.source_path + self.common_name, self.target_path + self.common_name)
skeleton_binary_thread = threading.Thread(
target=SkeletonBinary,
args=(source + name, target + name)
args=(self.source_path + self.common_name, self.target_path + self.common_name),
daemon=True,
)
ar_thread = threading.Thread(
target=ar.get_images,
args=()
daemon=True,
)
atlas_base64_thread = threading.Thread(
target=self.__atlas_to_base64,
args=(
self.file_paths["atlas"],
data,
".{}".format(self.config["server"]["operator_folder"]) + self.common_name + ".atlas",
),
daemon=True,
)
ar_thread.start()
skeleton_binary.start()
skeleton_binary_thread.start()
ar_thread.join()
atlas_base64_thread.start()
# alpha composite
for item in ar.images:
threading.Thread(
alpha_composite_thread = threading.Thread(
target=AlphaComposite,
args=(source + item, target + item)
).start()
args=(self.source_path + item, self.target_path + item),
daemon=True,
)
alpha_composite_threads.append(alpha_composite_thread)
alpha_composite_thread.start()
for thread in alpha_composite_threads:
thread.join()
for item in ar.images:
png_to_base64_thread = threading.Thread(
target=self.__png_to_base64,
args=(
self.target_path + item,
data,
".{}".format(self.config["server"]["operator_folder"]) + item,
),
daemon=True,
)
png_to_base64_threads.append(png_to_base64_thread)
png_to_base64_thread.start()
skeleton_binary_thread.join()
json_base64_thread =threading.Thread(
target=self.__json_to_base64,
args=(
self.file_paths["json"],
data,
".{}".format(self.config["server"]["operator_folder"]) + self.common_name + ".json",
),
daemon=True,
)
json_base64_thread.start()
json_base64_thread.join()
for thread in png_to_base64_threads:
thread.join()
atlas_base64_thread.join()
jsonContent = prefix + str(data)
with open(operator_file, "w") as f:
f.write(jsonContent)
print("Finished building operaotr data")
return
def __set_version(self):
version = input("Enter build version: ")
print(version)
return
def __json_to_base64(self, path, dict=None, key=None):
with open(pathlib.Path.cwd().joinpath(path), "r") as f:
data = f.read()
result = encode_string(data, type="application/json")
if dict is not None and key is not None:
dict[key] = result
else:
return result
def __atlas_to_base64(self, path, dict=None, key=None):
with open(pathlib.Path.cwd().joinpath(path), "r") as f:
data = f.read()
result = encode_string(data, type="application/octet-stream")
if dict is not None and key is not None:
dict[key] = result
else:
return result
def __png_to_base64(self, path, dict=None, key=None):
result = encode_image(path)
if dict is not None and key is not None:
dict[key] = result
else:
return result

View File

@@ -4,6 +4,23 @@ class Config:
def __init__(self) -> None:
self.config_path = pathlib.Path.cwd().joinpath("config.yaml")
self.valid_keys = dict(
config=dict(
server=dict,
operators=dict
),
server=dict(
template_folder=str,
release_folder=str,
operator_folder=str
),
operators=dict(
source_folder=str,
target_folder=str,
common_name=str,
release_folder=str
)
)
self.__read_config()
def __read_config(self):
@@ -15,29 +32,15 @@ class Config:
self.__validate_config()
def __validate_config(self):
config_keys = dict(
server=dict,
operators=dict
)
self.__config_check("config", self.config, config_keys)
key = "config"
self.__config_check(key, self.config, self.valid_keys[key])
server_keys = dict(
template_folder=str,
release_folder=str,
js_access_folder=str,
css_access_folder=str,
asset_access_folder=str,
version_file=str
)
self.__config_check("server", self.config["server"], server_keys)
key = "server"
self.__config_check(key, self.config[key], self.valid_keys[key])
operators_keys = dict(
source_folder=str,
target_folder=str,
common_name=str
)
for operator_name, operator_content in self.config["operators"].items():
self.__config_check(operator_name, operator_content, operators_keys)
key = "operators"
for operator_name, operator_content in self.config[key].items():
self.__config_check(operator_name, operator_content, self.valid_keys[key])
with open(self.config_path, 'w') as f:
yaml.safe_dump(self.config, f, allow_unicode=True)

View File

@@ -1,12 +1,47 @@
from http.server import SimpleHTTPRequestHandler
import pathlib
from socketserver import TCPServer
from lib.builder import Builder
class Server:
def __init__(self, port, config) -> None:
self.port = port
def __init__(self, port, operator, config) -> None:
self.config = config
self.operator = operator
self.port = port
self.httpd = TCPServer(("", port), httpd(operator, config["server"], directory=str(pathlib.Path.cwd())))
def start(self):
# build assets first
Builder(self.operator, self.config).build_assets()
print("Server is up at 0.0.0.0:{port}".format(port=self.port))
self.httpd.serve_forever()
return
def stop(self):
self.httpd.server_close()
return
class httpd(SimpleHTTPRequestHandler):
def __init__(self, operator, config, directory):
self.config = config
self.operator = operator
self.template_path = directory
def __call__(self, *args, **kwds):
super().__init__(*args, directory=self.template_path, **kwds)
def do_GET(self):
split_path = self.path.split("/")
access_path = "/{}/".format(split_path[1])
if self.path == "/":
self.path = self.config["template_folder"] + "index.html"
elif access_path == "/assets/":
# assets folder
self.path = self.config["template_folder"] + "assets/" + "/".join([i for i in split_path[2:]])
elif self.config["operator_folder"] == access_path:
# operator folder
self.path = self.config["operator_folder"] + "{}/".format(self.operator) + split_path[-1]
return SimpleHTTPRequestHandler.do_GET(self)

View File

@@ -62,7 +62,7 @@ CURVE_BEZIER = 2
class SkeletonBinary:
def __init__(self, file_path, save_path, scale=1) -> None:
def __init__(self, file_path, save_path, scale=1):
if file_path.strip().endswith(".skel") is False:
file_path += ".skel"
if save_path.strip().endswith(".json") is False:
@@ -78,7 +78,7 @@ class SkeletonBinary:
self.readSkeletonData()
with open(pathlib.Path.cwd().joinpath(save_path), "w") as f:
json.dump(self.dict, f, indent=2, sort_keys=True)
json.dump(self.dict, f)
except Exception as e:
print(e)
@@ -743,10 +743,12 @@ class SkeletonBinary:
if len(chars) < byteCount:
chars = [None] * byteCount
charCount = 0
for i in range(byteCount):
i = 0
while (i < byteCount):
b = self.read()
shiftedB = b >> 4
if shiftedB == -1:
# 0b11110000 -> 0b11111111 ?
raise ValueError("shiftedB is -1")
elif shiftedB == 12 or shiftedB == 13:
chars[charCount] = chr((b & 0x1F) << 6 | self.read() & 0x3F)

40
run.py
View File

@@ -1,40 +0,0 @@
import argparse
from lib.config import Config
from lib.server import Server
from lib.builder import Builder
class AkLive2D:
def __init__(self) -> None:
self.config = Config().config
self.args = None
self.server = None
self.builder = None
def start(self):
parser = argparse.ArgumentParser(description="Arknights Live 2D Wallpaper Builder")
arg_group = parser.add_mutually_exclusive_group(required=True)
arg_group.add_argument("-s", "--serve", dest="port", type=int, const=8080,nargs="?", help="Development server port (default: 8080)")
arg_group.add_argument("-b", "--build", dest="operator_name", type=str, const="all", nargs="?", help="Build wallpapers (default: all)")
self.args = parser.parse_args()
if self.args.port is not None:
self.server = Server(self.args.port, self.config["server"])
self.server.start()
if self.args.operator_name is not None:
self.builder = Builder(self.args.operator_name, self.config["operators"])
self.builder.start()
def stop(self):
return
if __name__ == "__main__":
aklive2d = AkLive2D()
try:
aklive2d.start()
except KeyboardInterrupt:
print("\nInterrupted, exiting...")
aklive2d.stop()

89
template/assets/runner.js Normal file
View File

@@ -0,0 +1,89 @@
const params = new URLSearchParams(window.location.search);
const RATIO = 0.618;
var fps = 60;
if (params.has("fps")) {
var tmp = parseInt(params.get("fps"));
if (!isNaN(tmp)) {
fps = tmp;
}
}
function supportsWebGL() {
try {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
return ctx != null;
} catch (e) {
return false;
}
}
function calculateScale(width, height) {
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let scaleX = windowWidth / width;
let scaleY = windowHeight / height;
return { x: scaleX, y: scaleY };
};
if (!supportsWebGL()) {
alert('WebGL is unavailable. Fallback image will be used.');
var e = document.getElementById("container");
e.classList.add("fallback");
e.parentElement.classList.add("widget-wrapper");
function fallback() {
const scale = calculateScale(window.settings.fallbackImage.width, window.settings.fallbackImage.height);
if (scale.x > scale.y) {
e.style.width = window.settings.fallbackImage.width * scale.y + "px";
e.style.height = window.settings.fallbackImage.height * scale.y + "px";
} else if (scale.x < scale.y) {
e.style.width = window.settings.fallbackImage.width * scale.x + "px";
e.style.height = window.settings.fallbackImage.height * scale.x + "px";
}
}
window.addEventListener('resize', fallback, true);
fallback();
} else {
var e = document.getElementById("container");
var resetTime = window.performance.now();
var spinePlayer = new spine.SpinePlayer(e, {
jsonUrl: window.settings.jsonUrl,
atlasUrl: window.settings.atlasUrl,
animation: window.settings.animation,
rawDataURIs: window.operator,
premultipliedAlpha: true,
alpha: true,
backgroundColor: "#00000000",
fps: fps,
viewport: window.settings.viewport,
showControls: false,
defaultMix: window.settings.defaultMix,
success: window.settings.success,
});
}
function resizeLogo() {
document.getElementById("logo").width = window.innerWidth / 2 * RATIO
}
window.addEventListener('resize', resizeLogo, true);
resizeLogo()
window.addEventListener("contextmenu", e => e.preventDefault());
document.addEventListener("gesturestart", e => e.preventDefault());
// wallpaper engine
window.wallpaperPropertyListener = {
applyGeneralProperties: function (properties) {
if (properties.fps) {
spinePlayer.setFps(properties.fps);
}
},
};
console.log("All resources are extracted from Arknights.")

View File

@@ -2089,11 +2089,15 @@ var spine;
this.errors = {};
this.toLoad = 0;
this.loaded = 0;
this.rawDataUris = {};
this.textureLoader = textureLoader;
this.pathPrefix = pathPrefix;
}
AssetManager.downloadText = function (url, success, error) {
AssetManager.prototype.downloadText = function (url, success, error) {
var request = new XMLHttpRequest();
request.overrideMimeType("text/html");
if (this.rawDataUris[url])
url = this.rawDataUris[url];
request.open("GET", url, true);
request.onload = function () {
if (request.status == 200) {
@@ -2108,8 +2112,10 @@ var spine;
};
request.send();
};
AssetManager.downloadBinary = function (url, success, error) {
AssetManager.prototype.downloadBinary = function (url, success, error) {
var request = new XMLHttpRequest();
if (this.rawDataUris[url])
url = this.rawDataUris[url];
request.open("GET", url, true);
request.responseType = "arraybuffer";
request.onload = function () {
@@ -2125,13 +2131,16 @@ var spine;
};
request.send();
};
AssetManager.prototype.setRawDataURI = function (path, data) {
this.rawDataUris[this.pathPrefix + path] = data;
};
AssetManager.prototype.loadText = function (path, success, error) {
var _this = this;
if (success === void 0) { success = null; }
if (error === void 0) { error = null; }
path = this.pathPrefix + path;
this.toLoad++;
AssetManager.downloadText(path, function (data) {
this.downloadText(path, function (data) {
_this.assets[path] = data;
if (success)
success(path, data);
@@ -2150,12 +2159,13 @@ var spine;
if (success === void 0) { success = null; }
if (error === void 0) { error = null; }
path = this.pathPrefix + path;
var storagePath = path;
this.toLoad++;
var img = new Image();
img.crossOrigin = "anonymous";
img.onload = function (ev) {
var texture = _this.textureLoader(img);
_this.assets[path] = texture;
_this.assets[storagePath] = texture;
_this.toLoad--;
_this.loaded++;
if (success)
@@ -2168,6 +2178,8 @@ var spine;
if (error)
error(path, "Couldn't load image " + path);
};
if (this.rawDataUris[path])
path = this.rawDataUris[path];
img.src = path;
};
AssetManager.prototype.loadTextureData = function (path, data, success, error) {
@@ -2201,7 +2213,7 @@ var spine;
var parent = path.lastIndexOf("/") >= 0 ? path.substring(0, path.lastIndexOf("/")) : "";
path = this.pathPrefix + path;
this.toLoad++;
AssetManager.downloadText(path, function (atlasData) {
this.downloadText(path, function (atlasData) {
var pagesLoaded = { count: 0 };
var atlasPages = new Array();
try {
@@ -9726,6 +9738,12 @@ var spine;
return dom;
}
this.assetManager = new spine.webgl.AssetManager(this.context);
if (config.rawDataURIs) {
for (var path in config.rawDataURIs) {
var data = config.rawDataURIs[path];
this.assetManager.setRawDataURI(path, data);
}
}
this.assetManager.loadText(config.jsonUrl);
this.assetManager.loadTextureAtlas(config.atlasUrl);
if (config.backgroundImage && config.backgroundImage.url)

43
template/assets/style.css Normal file
View File

@@ -0,0 +1,43 @@
html {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
height: 100%;
width: 100%;
overflow: hidden;
}
body {
margin: 0;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-image: url("../operator/operator_bg.png");
height: 100%;
width: 100%;
}
.logo {
position: fixed;
left: 0;
top: 0;
filter: invert(1);
opacity: 0.3;
z-index: -1;
}
.widget-wrapper {
display: flex;
align-items: center;
}
.fallback {
margin: auto;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-image: url("../operator/fallback.png");
width: 100%;
height: 100%;
}

View File

@@ -1,148 +0,0 @@
const params = new URLSearchParams(window.location.search);
const RATIO = 0.618;
var fps = 60;
if (params.has("fps")) {
var tmp = parseInt(params.get("fps"));
if (!isNaN(tmp)) {
fps = tmp;
}
}
function supportsWebGL() {
try {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
return ctx != null;
} catch (e) {
return false;
}
}
function calculateScale(width, height) {
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let scaleX = windowWidth / width;
let scaleY = windowHeight / height;
return { x: scaleX, y: scaleY };
};
if (!supportsWebGL()) {
alert('WebGL is unavailable. Fallback image will be used.');
var e = document.getElementById("spine-widget");
e.classList.add("fallback");
e.parentElement.classList.add("widget-wrapper");
document.getElementById("loader").style.display = "none";
function fallback() {
const scale = calculateScale(window.operator.fallbackImage.width, window.operator.fallbackImage.height);
if (scale.x > scale.y) {
e.style.width = window.operator.fallbackImage.width * scale.y;
e.style.height = window.operator.fallbackImage.height * scale.y;
} else if (scale.x < scale.y) {
e.style.width = window.operator.fallbackImage.width * scale.x;
e.style.height = window.operator.fallbackImage.height * scale.x;
}
document.getElementById("logo").width = window.innerWidth / 2 * RATIO
}
window.addEventListener('resize', fallback, true);
fallback();
} else {
var resetTime = window.performance.now();
var e = document.getElementById("spine-widget");
var spineWidget;
window.isLoaded = false;
function render() {
let isPlayingInteract = false;
var scale = calculateScale(window.operator.spineSize.width, window.operator.spineSize.height);
spineWidget = new spine.SpineWidget(e, {
atlasContent: atob(window.operator.atlasContent),
jsonContent: window.operator.jsonContent,
atlasPages: window.operator.atlasPages,
atlasPagesContent: window.operator.atlasPagesContent,
animation: "Idle",
backgroundColor: "#00000000",
loop: true,
skin: "default",
fps: fps,
scale: Math.min(scale.x, scale.y),
x: window.innerWidth / 2,
y: calculateCenterY(scale.x, scale.y),
fitToCanvas: false,
premultipliedAlpha: true,
success: function (widget) {
widget.state.addListener({
end: (e) => {
if (e.animation.name == "Interact") {
isPlayingInteract = false;
}
},
complete: (e) => {
if (window.performance.now() - resetTime >= 8 * 1000 && Math.random() < 0.3) {
resetTime = window.performance.now();
let entry = widget.state.setAnimation(0, "Special", true, 0);
entry.mixDuration = 0.2;
widget.state.addAnimation(0, "Idle", true, 0);
}
},
});
widget.canvas.onclick = function () {
if (isPlayingInteract) {
return;
}
isPlayingInteract = true;
let entry = widget.state.setAnimation(0, "Interact", true, 0);
entry.mixDuration = 0.2;
widget.state.addAnimation(0, "Idle", true, 0);
}
document.getElementById("loader").style.display = "none";
window.isLoaded = true;
}
});
document.getElementById("logo").width = window.innerWidth / 2 * RATIO
}
function calculateCenterY(scaleX, scaleY) {
var height = window.innerHeight;
var offset = Math.min(window.operator.spineSize.offset, height / window.operator.spineSize.correctionFactor);
if (scaleX < scaleY) {
// constrained by width
var scaledSpineHeight = window.operator.spineSize.height * scaleX;
return (height - scaledSpineHeight) / 2 + offset;
}
return offset;
}
render();
optimizedResize.add(function() {
if (window.isLoaded) {
window.isLoaded = false;
document.getElementById("logo").width = window.innerWidth / 2 * RATIO
document.getElementById("loader").style.display = "inherit";
var scale = calculateScale(window.operator.spineSize.width, window.operator.spineSize.height);
spineWidget.reRender({
x: window.innerWidth / 2,
y: calculateCenterY(scale.x, scale.y),
scale: Math.min(scale.x, scale.y),
});
}
});
}
window.addEventListener("contextmenu", e => e.preventDefault());
document.addEventListener("gesturestart", e => e.preventDefault());
// wallpaper engine
window.wallpaperPropertyListener = {
applyGeneralProperties: function (properties) {
if (properties.fps) {
spineWidget.setFps(properties.fps);
}
},
};
console.log("All resources are extracted from Arknights.")

File diff suppressed because it is too large Load Diff

View File

@@ -1,117 +0,0 @@
html {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
height: 100%;
width: 100%;
overflow: hidden;
}
body {
margin: 0;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-image: url("../assets/operator_bg.png");
height: 100%;
width: 100%;
}
.logo {
position: fixed;
left: 0;
top: 0;
filter: invert(1);
opacity: 0.3;
z-index: -1;
}
.widget-wrapper {
display: flex;
align-items: center;
height: 100%;
width: 100%;
}
.fallback {
margin: auto;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-image: url("../assets/fallback.png");
width: 100%;
height: 100%;
}
.loader,
.loader:before,
.loader:after {
background: #546e7a;
-webkit-animation: load1 1s infinite ease-in-out;
animation: load1 1s infinite ease-in-out;
width: 1em;
height: 4em;
}
.loader {
color: #546e7a;
text-indent: -9999em;
margin: 88px auto;
position: relative;
font-size: 11px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
position: fixed;
right: 88px;
top: 0;
z-index: 100;
}
.loader:before,
.loader:after {
position: absolute;
top: 0;
content: '';
}
.loader:before {
left: -1.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 1.5em;
}
@-webkit-keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}
@keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}

View File

@@ -1,4 +1,5 @@
<html lang="en">
<!doctype html>
<html>
<head>
<meta charset="utf-8">
@@ -6,19 +7,21 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="renderer" content="webkit">
<title>Skadi the Corrupting Heart</title>
<link rel="stylesheet" type="text/css" href="./build/style.css?v1.6.1">
<link rel="stylesheet" href="./assets/style.css">
<link rel="stylesheet" href="./assets/spine-player.css">
</head>
<body>
<div id="dev" style="position: fixed;left: 0;left: 0;background-color: white;"></div>
<div class="loader" id="loader">Loading...</div>
<image src="./assets/operator_logo.png" class="logo" id="logo" alt="operator logo" />
<div id="widget-wrapper">
<div id="spine-widget"></div>
<image src="./operator/operator_logo.png" class="logo" id="logo" alt="operator logo" />
<div id="widget-wrapper" style="width: 100%; height: 100%;">
<div id="container" style="width: 100%; height: 100%;"></div>
</div>
</body>
<script type="text/javascript" src="./build/operator.js?v1.6.1"></script>
<script type="text/javascript" src="./build/spine-widget.js?v1.6.1"></script>
<script type="text/javascript" src="./build/runner.js?v1.6.1"></script>
<script src="./assets/spine-player.js"></script>
<script type="text/javascript" src="./operator/operator.js"></script>
<script type="text/javascript" src="./operator/settings.js"></script>
<script type="text/javascript" src="./assets/runner.js"></script>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

View File

@@ -1,148 +0,0 @@
const params = new URLSearchParams(window.location.search);
const RATIO = 0.618;
var fps = 60;
if (params.has("fps")) {
var tmp = parseInt(params.get("fps"));
if (!isNaN(tmp)) {
fps = tmp;
}
}
function supportsWebGL() {
try {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
return ctx != null;
} catch (e) {
return false;
}
}
function calculateScale(width, height) {
let windowWidth = window.innerWidth;
let windowHeight = window.innerHeight;
let scaleX = windowWidth / width;
let scaleY = windowHeight / height;
return { x: scaleX, y: scaleY };
};
if (!supportsWebGL()) {
alert('WebGL is unavailable. Fallback image will be used.');
var e = document.getElementById("spine-widget");
e.classList.add("fallback");
e.parentElement.classList.add("widget-wrapper");
document.getElementById("loader").style.display = "none";
function fallback() {
const scale = calculateScale(window.operator.fallbackImage.width, window.operator.fallbackImage.height);
if (scale.x > scale.y) {
e.style.width = window.operator.fallbackImage.width * scale.y;
e.style.height = window.operator.fallbackImage.height * scale.y;
} else if (scale.x < scale.y) {
e.style.width = window.operator.fallbackImage.width * scale.x;
e.style.height = window.operator.fallbackImage.height * scale.x;
}
document.getElementById("logo").width = window.innerWidth / 2 * RATIO
}
window.addEventListener('resize', fallback, true);
fallback();
} else {
var resetTime = window.performance.now();
var e = document.getElementById("spine-widget");
var spineWidget;
window.isLoaded = false;
function render() {
let isPlayingInteract = false;
var scale = calculateScale(window.operator.spineSize.width, window.operator.spineSize.height);
spineWidget = new spine.SpineWidget(e, {
atlasContent: atob(window.operator.atlasContent),
json: "./assets/dyn_illust_char_1012_skadi2.json",
atlasPages: window.operator.atlasPages,
atlasPagesContent: window.operator.atlasPagesContent,
animation: "Idle",
backgroundColor: "#00000000",
loop: true,
skin: "default",
fps: fps,
scale: Math.min(scale.x, scale.y),
x: window.innerWidth / 2,
y: calculateCenterY(scale.x, scale.y),
fitToCanvas: false,
premultipliedAlpha: true,
success: function (widget) {
widget.state.addListener({
end: (e) => {
if (e.animation.name == "Interact") {
isPlayingInteract = false;
}
},
complete: (e) => {
if (window.performance.now() - resetTime >= 8 * 1000 && Math.random() < 0.3) {
resetTime = window.performance.now();
let entry = widget.state.setAnimation(0, "Special", true, 0);
entry.mixDuration = 0.2;
widget.state.addAnimation(0, "Idle", true, 0);
}
},
});
widget.canvas.onclick = function () {
if (isPlayingInteract) {
return;
}
isPlayingInteract = true;
let entry = widget.state.setAnimation(0, "Interact", true, 0);
entry.mixDuration = 0.2;
widget.state.addAnimation(0, "Idle", true, 0);
}
document.getElementById("loader").style.display = "none";
window.isLoaded = true;
}
});
document.getElementById("logo").width = window.innerWidth / 2 * RATIO
}
function calculateCenterY(scaleX, scaleY) {
var height = window.innerHeight;
var offset = Math.min(window.operator.spineSize.offset, height / window.operator.spineSize.correctionFactor);
if (scaleX < scaleY) {
// constrained by width
var scaledSpineHeight = window.operator.spineSize.height * scaleX;
return (height - scaledSpineHeight) / 2 + offset;
}
return offset;
}
render();
optimizedResize.add(function() {
if (window.isLoaded) {
window.isLoaded = false;
document.getElementById("logo").width = window.innerWidth / 2 * RATIO
document.getElementById("loader").style.display = "inherit";
var scale = calculateScale(window.operator.spineSize.width, window.operator.spineSize.height);
spineWidget.reRender({
x: window.innerWidth / 2,
y: calculateCenterY(scale.x, scale.y),
scale: Math.min(scale.x, scale.y),
});
}
});
}
window.addEventListener("contextmenu", e => e.preventDefault());
document.addEventListener("gesturestart", e => e.preventDefault());
// wallpaper engine
window.wallpaperPropertyListener = {
applyGeneralProperties: function (properties) {
if (properties.fps) {
spineWidget.setFps(properties.fps);
}
},
};
console.log("All resources are extracted from Arknights.")

View File

@@ -1,117 +0,0 @@
html {
-ms-user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
height: 100%;
width: 100%;
overflow: hidden;
}
body {
margin: 0;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-image: url("../assets/operator_bg.png");
height: 100%;
width: 100%;
}
.logo {
position: fixed;
left: 0;
top: 0;
filter: invert(1);
opacity: 0.3;
z-index: -1;
}
.widget-wrapper {
display: flex;
align-items: center;
height: 100%;
width: 100%;
}
.fallback {
margin: auto;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-image: url("../assets/fallback.png");
width: 100%;
height: 100%;
}
.loader,
.loader:before,
.loader:after {
background: #546e7a;
-webkit-animation: load1 1s infinite ease-in-out;
animation: load1 1s infinite ease-in-out;
width: 1em;
height: 4em;
}
.loader {
color: #546e7a;
text-indent: -9999em;
margin: 88px auto;
position: relative;
font-size: 11px;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
position: fixed;
right: 88px;
top: 0;
z-index: 100;
}
.loader:before,
.loader:after {
position: absolute;
top: 0;
content: '';
}
.loader:before {
left: -1.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 1.5em;
}
@-webkit-keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}
@keyframes load1 {
0%,
80%,
100% {
box-shadow: 0 0;
height: 4em;
}
40% {
box-shadow: 0 -2em;
height: 5em;
}
}

View File

@@ -1,77 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script src="./build/spine-player.js"></script>
<link rel="stylesheet" href="./build/style.css">
<link rel="stylesheet" href="./build/spine-player.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="dev" style="position: fixed;left: 0;left: 0;background-color: white;"></div>
<div id="container" style="width: 100%; height: 100%;"></div>
</body>
<script>
const params = new URLSearchParams(window.location.search);
var fps = 60;
if (params.has("fps")) {
var tmp = parseInt(params.get("fps"));
if (!isNaN(tmp)) {
fps = tmp;
}
}
var resetTime = window.performance.now();
let isPlayingInteract = false;
new spine.SpinePlayer("container", {
jsonUrl: "assets/dyn_illust_char_1012_skadi2.json",
atlasUrl: "assets/dyn_illust_char_1012_skadi2.atlas",
animation: "Idle",
premultipliedAlpha: true,
alpha: true,
backgroundColor: "#00000000",
fps: fps,
viewport: {
debugRender: false,
padLeft: "-5%",
padRight: "-10%",
padTop: "0%",
padBottom: "-12%",
x: 0,
y: 0,
},
showControls: false,
success: function (widget) {
widget.animationState.addListener({
end: (e) => {
if (e.animation.name == "Interact") {
isPlayingInteract = false;
}
},
complete: (e) => {
if (window.performance.now() - resetTime >= 8 * 1000 && Math.random() < 0.3) {
resetTime = window.performance.now();
let entry = widget.animationState.setAnimation(0, "Special", true, 0);
entry.mixDuration = 0.2;
widget.animationState.addAnimation(0, "Idle", true, 0);
}
},
});
widget.canvas.onclick = function () {
if (isPlayingInteract) {
return;
}
isPlayingInteract = true;
let entry = widget.animationState.setAnimation(0, "Interact", true, 0);
entry.mixDuration = 0.2;
widget.animationState.addAnimation(0, "Idle", true, 0);
}
}
});
</script>
</body>
</html>