在你的博客里放一只可爱的Spine Model吧~

一切的开始 万恶之源 是某Tover说想在他博客上养一只Miku,还想要小的,然后……本着自己的npy能宠一定宠的原则,就去把pjsk资源解包+解密了(具体请自己折腾,版权问题本文不提供现成资源)。

然而没想到解出来发现小Miku的模型是用spine做的,而能找到的绝大多数文章都是在hexo里添加live2d而不是spine,于是开始了漫漫魔改路。得益于之前的魔改把Melody主题的结构摸清楚了,才能知道哪些代码放哪里怎么用hhh(折腾只有一次和无数次?

效果展示:https://c10udlnk.top

image-20211101191625607

参考文章

Before all

一个spine模型应该有以下几个必备文件:

  • xxx.skel:骨架
  • xxx.png:贴图
  • xxx.atlas:纹理图集

主题文件的源目录中(例如Melody主题的路径是blog/node_modules/hexo-theme-melody,Butterfly的则是在Blog/themes/butterfly中)新增一个名为spine_models的文件夹,并将以上文件放于同一子目录中(下图示例是sd_21miku_normal_r文件夹)。

spine_models文件夹下新建一个随机长度的空语音default.mp3(为了显示文本而无声音,将在后续改进中大修,避免此无用操作)。

image-20211102194122693

更改_config.yml

_config.yml最后新增以下部分,具体什么配置改什么都写在注释里了。

其中animations(动画名称)和skin(皮肤)可以用spine对应版本的Skeleton Viewer来看,比如Miku的是3.6.53的spine,那用skeletonViewer-3.6.53.jar可以看到:

image-20211102192727985

_config.yml中增加的代码:

myspine:
  enable: true #是否启用
  spineDir: "/spine_models/" #spine模型存放的主目录,上面说的新建的spine_models
  models:
    - name: "sd_21miku_normal_r/" #模型存放子文件夹名(末尾一定要带斜杠)
      skin: "default" #使用的皮肤
      atlas: "sekai_atlas.atlas" #atlas文件的url
      skeleton: "sekai_atlas.skel" #skeleton文件的url
    - name: "model_example/" #可增加多个模型
      skin: "model_example.skin"
      atlas: "model_example.atlas"
      skeleton: "model_example.skel"
  styles:
    widget: #展示窗口的样式,可以自行添加其他需要设置的CSS属性
      width: "200px"
      height: "200px"
    voiceText: #语音文字的样式,可以自行添加其他需要设置的CSS属性
      color: "#e6e6e6"
  behaviors:
    start: #模型刚出现时触发的行为
      animation: "w_emu_run01_f" #动画名称
      voice: "" #音频的url,为空默认是default.mp3
      text: "Ohhhhh好耶" #语音文字
    idle: #模型被闲置时触发的行为
      maxMinutes: 1 #最大闲置时间,不够时间将循环播放当前动画,时间到达后将随机替换一个闲置动画
      animations:
        - name: "m_cool_idle01_f" #动画名称,可增加多个动画
          loop: false #语音结束前是否循环播放
        - name: "m_cool_joy01_f"
          loop: false
      voices: 
        - voice: "" #同上,可增加多个语音
          text: "不买立省百分百" #同上
    interact: #模型被点击时触发的行为
      maxPlaySec: 3 #单个动画的最长播放时间,超过将直接停止播放
      animations:
        - name: "w_normal_joy01_b" #同上
          loop: false #同上
        - name: "m_cool_angry01_f"
          loop: false
      voices:
        - voice: "" #同上
          text: "既见未来,为何不buy" #同上

使用Hexo自带的注入器

将spine相关的html使用Hexo Injector注入到网页的body部分,将以下代码保存为myspine-injector.js,然后放在主题文件的源目录scripts目录下。

hexo.extend.injector.register('body_end', function () {
    const {
        enable,
        spineDir,
        models,
        styles,
        behaviors
    } = hexo.config.myspine;

    if (!enable) {
        return null;
    }

    return `
    <div class="myspine-spine-widget"></div>
    <script src="/js/third-party/myspine/spine-widget.js"></script>
    <script src="/js/third-party/myspine/spine-skeleton-binary.js"></script>
    <script src="/js/third-party/myspine/myspine.js"></script>
    <link type="text/css" href="/css/_third-party/myspine.css"></link>
    <script>
        new MySpine({
            spineDir: "${spineDir}",
            models: ${JSON.stringify(models)},
            styles: ${JSON.stringify(styles)},
            behaviors: ${JSON.stringify(behaviors)}
        });
    </script>
    `
})

image-20211102194050555

spine引擎

主题文件的源目录source/js/third-party/myspine路径下放置对应spine版本spine-widget.jsspine-skeleton-binary.js

image-20211102195054233

spine-widget.js

比如3.6.53版本的可以在https://github.com/EsotericSoftware/spine-runtimes/blob/3.6.53/spine-ts/build/spine-widget.js里下载。

不同版本的spine改中间的版本号就行。

image-20211102195156304

spine-skeleton-binary.js

这个我死活找不到按版本号分类的,参考文章里给的是3.5.51版的spine-skeleton-binary.js

鉴于这个文件是用来解析skel文件二进制的,逆向手思索了一下,突然反应过来Skeleton Viewer不也是解析skel文件二进制诶,那不如……用jd-gui逆了一下skeletonViewer-3.6.53.jarskeletonViewer-3.5.51.jar,对比发现3.6.53多了点东西,在原js的基础上加了对应的解析就可了。

更改后3.6.53版本的spine-skeleton-binary.js

(function (spine) {
    let BlendMode = ["normal", "additive", "multiply", "screen"];
    let AttachmentType = ["region", "boundingbox", "mesh", "linkedmesh", "path", "point", "clipping"];
    let TransformMode = ["normal", "onlyTranslation", "noRotationOrReflection", "noScale", "noScaleOrReflection"];
    let PositionMode = ["fixed", "percent"];
    let SpacingMode = ["length", "fixed", "percent"];
    let RotateMode = ["tangent", "chain", "chainScale"];

    spine.SkeletonJsonConverter = (function () {
        function SkeletonJsonConverter(data, scale = 1) {
            this.data = data;
            this.scale = scale;
            this.nextNum = 0;
            this.chars = null;
            this.json = {};
        }
        SkeletonJsonConverter.prototype.convertToJson = function () {
            this.json.skeleton = {};
            let skeleton = this.json.skeleton;
            skeleton.hash = this.readString();
            if (skeleton.hash.length == 0x0) skeleton.hash = null;
            skeleton.spine = this.readString();
            if (skeleton.spine.length == 0x0) skeleton.spine = null;
            skeleton.width = this.readFloat();
            skeleton.height = this.readFloat();
            let nonessential = this.readBoolean();
            if (nonessential) {
                skeleton.fps = this.readFloat();
                skeleton.images = this.readString();
                if (skeleton.images.length == 0x0) skeleton.images = null;
            }
            this.json.bones = new Array(this.readInt(!![]));
            let bones = this.json.bones;
            for (let i = 0x0; i < bones.length; i++) {
                let boneData = {};
                boneData.name = this.readString();
                boneData.parent = null;
                if (i != 0x0) {
                    const nonessential = this.readInt(!![]);
                    boneData.parent = bones[nonessential].name;
                }
                boneData.rotation = this.readFloat();
                boneData["x"] = this.readFloat() * this.scale;
                boneData["y"] = this.readFloat() * this.scale;
                boneData.scaleX = this.readFloat();
                boneData.scaleY = this.readFloat();
                boneData.shearX = this.readFloat();
                boneData.shearY = this.readFloat();
                boneData.length = this.readFloat() * this.scale;
                boneData.transform = TransformMode[this.readInt(!![])];
                if (nonessential) {
                    boneData.color = this.readColor();
                }
                bones[i] = boneData;
            }
            this.json.slots = new Array(this.readInt(!![]));
            let slots = this.json.slots;
            for (let i = 0x0; i < slots.length; i++) {
                let slotData = {};
                slotData.name = this.readString();
                const boneData = this.json.bones[this.readInt(!![])];
                slotData.bone = boneData.name;
                slotData.color = this.readColor();
                slotData.darkColor = this.readColor();
                slotData.attachment = this.readString();
                slotData.blend = BlendMode[this.readInt(!![])];
                slots[i] = slotData;
            }
            this.json["ik"] = new Array(this.readInt(!![]));
            let ik = this.json["ik"];
            for (let i = 0x0; i < ik.length; i++) {
                let ikConstraints = {};
                ikConstraints.name = this.readString();
                ikConstraints.order = this.readInt(!![]);
                ikConstraints.bones = new Array(this.readInt(!![]));
                for (let j = 0x0; j < ikConstraints.bones.length; j++) {
                    ikConstraints.bones[j] = this.json.bones[this.readInt(!![])].name;
                }
                ikConstraints.target = this.json.bones[this.readInt(!![])].name;
                ikConstraints.mix = this.readFloat();
                ikConstraints.bendPositive = this.readByte() != 0xff;
                ik[i] = ikConstraints;
            }
            this.json.transform = new Array(this.readInt(!![]));
            let transform = this.json.transform;
            for (let i = 0x0; i < transform.length; i++) {
                let transformData = {};
                transformData.name = this.readString();
                transformData.order = this.readInt(!![]);
                const bones = new Array(this.readInt(!![]));
                for (let j = 0x0, len = bones.length; j < len; j++) {
                    bones[j] = this.json.bones[this.readInt(!![])].name;
                }
                transformData.bones = bones;
                transformData.target = this.json.bones[this.readInt(!![])].name;
                transformData.rotation = this.readFloat();
                transformData["x"] = this.readFloat() * this.scale;
                transformData["y"] = this.readFloat() * this.scale;
                transformData.scaleX = this.readFloat();
                transformData.scaleY = this.readFloat();
                transformData.shearY = this.readFloat();
                transformData.rotateMix = this.readFloat();
                transformData.translateMix = this.readFloat();
                transformData.scaleMix = this.readFloat();
                transformData.shearMix = this.readFloat();
                transform[i] = transformData;
            }
            this.json.path = new Array(this.readInt(!![]));
            let path = this.json.path;
            for (let i = 0x0; i < path.length; i++) {
                let pathData = {};
                pathData.name = this.readString();
                pathData.order = this.readInt(!![]);
                pathData.bones = new Array(this.readInt(!![]));
                for (let j = 0x0, len = pathData.bones.length; j < len; j++) {
                    pathData.bones[j] = this.json.bones[this.readInt(!![])].name;
                }
                pathData.target = this.json.slots[this.readInt(!![])].name;
                pathData.positionMode = PositionMode[this.readInt(!![])];
                pathData.spacingMode = SpacingMode[this.readInt(!![])];
                pathData.rotateMode = RotateMode[this.readInt(!![])];
                pathData.rotation = this.readFloat();
                pathData.position = this.readFloat();
                if (pathData.positionMode == "fixed") {
                    pathData.position *= this.scale;
                }
                pathData.spacing = this.readFloat();
                if (pathData.spacingMode == "length" || pathData.spacingMode == "fixed") {
                    pathData.spacing *= this.scale;
                }
                pathData.rotateMix = this.readFloat();
                pathData.translateMix = this.readFloat();
                path[i] = pathData;
            }
            this.json.skins = {};
            this.json.skinsName = new Array();
            let skins = this.json.skins;
            let skinData = this.readSkin("default", nonessential);
            if (skinData != null) {
                skins.default = skinData;
                this.json.skinsName.push("default");
            }
            for (let i = 0x0, len = this.readInt(!![]); i < len; i++) {
                let skinName = this.readString();
                let skin = this.readSkin(skinName, nonessential);
                skins[skinName] = skin;
                this.json.skinsName.push(skinName);
            }
            this.json.events = [];
            this.json.eventsName = [];
            let events = this.json.events;
            for (let i = 0x0, len = this.readInt(!![]); i < len; i++) {
                let eventName = this.readString();
                let event = {};
                event.int = this.readInt(![]);
                event.float = this.readFloat();
                event.string = this.readString();
                events[eventName] = event;
                this.json.eventsName[i] = eventName;
            }
            this.json.animations = {};
            let animations = this.json.animations;
            for (let i = 0x0, len = this.readInt(!![]); i < len; i++) {
                let animationName = this.readString();
                let animation = this.readAnimation(animationName);
                animations[animationName] = animation;
            }
        };
        SkeletonJsonConverter.prototype.readByte = function () {
            return this.nextNum < this.data.length ? this.data[this.nextNum++] : null;
        };
        SkeletonJsonConverter.prototype.readBoolean = function () {
            return this.readByte() != 0x0;
        };
        SkeletonJsonConverter.prototype.readShort = function () {
            return (this.readByte() << 0x8) | this.readByte();
        };
        SkeletonJsonConverter.prototype.readInt = function (optimizePositive) {
            if (typeof optimizePositive === "undefined") {
                return (
                    (this.readByte() << 0x18) |
                    (this.readByte() << 0x10) |
                    (this.readByte() << 0x8) |
                    this.readByte()
                );
            }
            let b = this.readByte();
            let result = b & 0x7f;
            if ((b & 0x80) != 0x0) {
                b = this.readByte();
                result |= (b & 0x7f) << 0x7;
                if ((b & 0x80) != 0x0) {
                    b = this.readByte();
                    result |= (b & 0x7f) << 0xe;
                    if ((b & 0x80) != 0x0) {
                        b = this.readByte();
                        result |= (b & 0x7f) << 0x15;
                        if ((b & 0x80) != 0x0) {
                            b = this.readByte();
                            result |= (b & 0x7f) << 0x1c;
                        }
                    }
                }
            }
            return optimizePositive ? result : (result >> 0x1) ^ -(result & 0x1);
        };
        SkeletonJsonConverter.prototype.bytes2Float32 = function (bytes) {
            let sign = bytes & 0x80000000 ? -0x1 : 0x1;
            let exponent = ((bytes >> 0x17) & 0xff) - 0x7f;
            let significand = bytes & ~(-0x1 << 0x17);
            if (exponent == 0x80)
                return sign * (significand ? Number.NaN : Number.POSITIVE_INFINITY);
            if (exponent == -0x7f) {
                if (significand == 0x0) return sign * 0x0;
                exponent = -0x7e;
                significand /= 0x1 << 0x16;
            } else significand = (significand | (0x1 << 0x17)) / (0x1 << 0x17);
            return sign * significand * Math.pow(0x2, exponent);
        };
        SkeletonJsonConverter.prototype.readFloat = function () {
            return this.bytes2Float32(
                (this.readByte() << 0x18) +
                (this.readByte() << 0x10) +
                (this.readByte() << 0x8) +
                (this.readByte() << 0x0)
            );
        };
        SkeletonJsonConverter.prototype.readVertices = function (vertexCount) {
            let verticesLength = vertexCount << 0x1;
            if (!this.readBoolean()) {
                return this.readFloatArray(verticesLength, this.scale);
            }
            let bonesArray = new Array();
            for (let i = 0x0; i < vertexCount; i++) {
                let dataLength = this.readInt(!![]);
                bonesArray.push(dataLength);
                for (let j = 0x0; j < dataLength; j++) {
                    bonesArray.push(this.readInt(!![]));
                    bonesArray.push(this.readFloat() * this.scale);
                    bonesArray.push(this.readFloat() * this.scale);
                    bonesArray.push(this.readFloat());
                }
            }
            return bonesArray;
        };
        SkeletonJsonConverter.prototype.readFloatArray = function (length, scale) {
            let array = new Array(length);
            if (scale == 0x1) {
                for (let i = 0x0; i < length; i++) {
                    array[i] = this.readFloat();
                }
            } else {
                for (let i = 0x0; i < length; i++) {
                    array[i] = this.readFloat() * scale;
                }
            }
            return array;
        };
        SkeletonJsonConverter.prototype.readShortArray = function () {
            let n = this.readInt(!![]);
            let array = new Array(n);
            for (let i = 0x0; i < n; i++) {
                array[i] = this.readShort();
            }
            return array;
        };
        SkeletonJsonConverter.prototype.readIntArray = function () {
            let n = this.readInt(!![]);
            let array = new Array(n);
            for (let i = 0x0; i < n; i++) array[i] = this.readInt(!![]);
            return array;
        };
        SkeletonJsonConverter.prototype.readHex = function () {
            let hex = this.readByte().toString(0x10);
            return hex.length == 0x2 ? hex : "0" + hex;
        };
        SkeletonJsonConverter.prototype.readColor = function () {
            return this.readHex() + this.readHex() + this.readHex() + this.readHex();
        };
        SkeletonJsonConverter.prototype.readString = function () {
            let charCount = this.readInt(this, !![]);
            switch (charCount) {
                case 0x0:
                    return null;
                case 0x1:
                    return "";
            }
            charCount--;
            this.chars = "";
            let charIndex = 0x0;
            for (let i = 0x0; i < charCount;) {
                charIndex = this.readByte();
                switch (charIndex >> 0x4) {
                    case 0xc:
                    case 0xd:
                        this.chars += String.fromCharCode(
                            ((charIndex & 0x1f) << 0x6) | (this.readByte() & 0x3f)
                        );
                        i += 0x2;
                        break;
                    case 0xe:
                        this.chars += String.fromCharCode(
                            ((charIndex & 0xf) << 0xc) |
                            ((this.readByte() & 0x3f) << 0x6) |
                            (this.readByte() & 0x3f)
                        );
                        i += 0x3;
                        break;
                    default:
                        this.chars += String.fromCharCode(charIndex);
                        i++;
                }
            }
            return this.chars;
        };
        SkeletonJsonConverter.prototype.readSkin = function (slotIndex, nonessential) {
            let slotCount = this.readInt(!![]);
            if (slotCount == 0x0) return null;
            let skin = {};
            for (let i = 0x0; i < slotCount; i++) {
                const slotIndex = this.readInt(!![]);
                const slot = {};
                for (let j = 0x0, n = this.readInt(!![]); j < n; j++) {
                    let name = this.readString();
                    let attachment = this.readAttachment(name, nonessential);
                    if (attachment != null) {
                        slot[name] = attachment;
                    }
                }
                skin[this.json.slots[slotIndex].name] = slot;
            }
            return skin;
        };
        SkeletonJsonConverter.prototype.readAttachment = function (attachmentName, nonessential) {
            let scale = this.scale;
            let name = this.readString();
            if (name == null) name = attachmentName;

            let path,
                n,
                region = {},
                box = {},
                mesh = {},
                linkdeMesh = {};
            let array;
            let point = {};
            let clipping = {};

            switch (AttachmentType[this.readByte()]) {
                case "region":
                    path = this.readString();
                    if (path == null) path = name;
                    region.type = "region";
                    region.name = name;
                    region.path = path.trim();
                    region.rotation = this.readFloat();
                    region["x"] = this.readFloat() * scale;
                    region["y"] = this.readFloat() * scale;
                    region.scaleX = this.readFloat();
                    region.scaleY = this.readFloat();
                    region.width = this.readFloat() * scale;
                    region.height = this.readFloat() * scale;
                    region.color = this.readColor();
                    return region;
                case "boundingbox":
                    box.type = "boundingbox";
                    box.name = name;
                    n = this.readInt(!![]);
                    box.vertexCount = n;
                    box.vertices = this.readVertices(n);
                    if (this.nonessential) {
                        box.color = this.readColor();
                    }
                    return box;
                case "mesh":
                    path = this.readString();
                    if (path == null) path = name;
                    mesh.type = "mesh";
                    mesh.name = name;
                    mesh.path = path;
                    mesh.color = this.readColor();
                    n = this.readInt(!![]);
                    mesh.uvs = this.readFloatArray(n << 0x1, 0x1);
                    mesh.triangles = this.readShortArray();
                    mesh.vertices = this.readVertices(n);
                    mesh.hull = this.readInt(!![]) << 0x1;
                    if (nonessential) {
                        mesh.edges = this.readShortArray();
                        mesh.width = this.readFloat() * scale;
                        mesh.height = this.readFloat() * scale;
                    }
                    return mesh;
                case "linkedmesh":
                    path = this.readString();
                    if (path == null) path = name;
                    linkdeMesh.type = "linkedmesh";
                    linkdeMesh.name = name;
                    linkdeMesh.path = path;
                    linkdeMesh.color = this.readColor();
                    linkdeMesh.skin = this.readString();
                    linkdeMesh.parent = this.readString();
                    linkdeMesh.deform = this.readBoolean();
                    if (nonessential) {
                        linkdeMesh.width = this.readFloat() * scale;
                        linkdeMesh.height = this.readFloat() * scale;
                    }
                    return linkdeMesh;
                case "path":
                    path = {};
                    path.type = "path";
                    path.name = name;
                    path.closed = this.readBoolean();
                    path.constantSpeed = this.readBoolean();
                    n = this.readInt(!![]);
                    path.vertexCount = n;
                    path.vertices = this.readVertices(n);
                    array = array = new Array(n / 0x3);
                    for (let i = 0x0; i < array.length; i++) {
                        array[i] = this.readFloat() * scale;
                    }
                    path.lengths = array;
                    if (nonessential) {
                        path.color = this.readColor();
                    }
                    return path;
                case "point":
                    point.type = "point";
                    point.name = name;
                    point.rotation = this.readFloat();
                    point["x"] = this.readFloat() * scale;
                    point["y"] = this.readFloat() * scale;
                    if (nonessential) {
                        path.color = this.readColor();
                    }
                    return point;
                case "clipping":
                    clipping.type = "clipping";
                    clipping.name = name;
                    clipping.end = this.readInt(!![]);
                    n = this.readInt(!![]);
                    clipping.vertexCount = n;
                    clipping.vertices = this.readVertices(n);
                    if (nonessential) {
                        clipping.color = this.readColor();
                    }
                    return clipping;
            }
            return null;
        };
        SkeletonJsonConverter.prototype.readCurve = function (frameIndex, timeline) {
            let cx1, cy1, cx2, cy2;
            switch (this.readByte()) {
                case 0x0:
                    timeline[frameIndex].curve = "linear";
                    break;
                case 0x1:
                    timeline[frameIndex].curve = "stepped";
                    break;
                case 0x2:
                    cx1 = this.readFloat();
                    cy1 = this.readFloat();
                    cx2 = this.readFloat();
                    cy2 = this.readFloat();
                    timeline[frameIndex].curve = [cx1, cy1, cx2, cy2];
            }
        };
        SkeletonJsonConverter.prototype.readAnimation = function (name) {
            let animation = {};
            let scale = this.scale;
            let duration = 0x0;
            let slots = {};
            for (let i = 0x0, nn = this.readInt(!![]); i < nn; i++) {
                let slotIndex = this.readInt(!![]);
                let slotMap = {};
                let timeCount = this.readInt(!![]);
                for (let ii = 0x0; ii < timeCount; ii++) {
                    let timelineType = this.readByte();
                    let frameCount = this.readInt(!![]);
                    let timeline;
                    switch (timelineType) {
                        case 0x0:
                            timeline = new Array(frameCount);
                            for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                                let time = this.readFloat();
                                let color = this.readString();
                                timeline[frameIndex] = {};
                                timeline[frameIndex].time = time;
                                timeline[frameIndex].name = color;
                            }
                            slotMap.attachment = timeline;
                            duration = Math.max(duration, timeline[frameCount - 0x1].time);
                            break;
                        case 0x1:
                            timeline = new Array(frameCount);
                            for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                                const time = this.readFloat();
                                const color = this.readColor();
                                timeline[frameIndex] = {};
                                timeline[frameIndex].time = time;
                                timeline[frameIndex].color = color;
                                if (frameIndex < frameCount - 0x1) {
                                    this.readCurve(frameIndex, timeline); //let curve
                                }
                            }
                            slotMap.color = timeline;
                            duration = Math.max(duration, timeline[frameCount - 0x1].time);
                            break;
                        case 0x2:
                            timeline = new Array(frameCount);
                            for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                                const time = this.readFloat();
                                let lightColor = this.readColor();
                                let darkColor = this.readColor();
                                timeline[frameIndex] = {};
                                timeline[frameIndex].time = time;
                                timeline[frameIndex].light = lightColor;
                                timeline[frameIndex].dark = darkColor;
                                if (frameIndex < frameCount - 0x1) {
                                    this.readCurve(frameIndex, timeline); // let curve
                                }
                            }
                            slotMap.twoColor = timeline;
                            duration = Math.max(duration, timeline[frameCount - 0x1].time);
                            break;
                    }
                }
                slots[this.json.slots[slotIndex].name] = slotMap;
            }
            animation.slots = slots;
            let bones = {};
            for (let i = 0x0, n = this.readInt(!![]); i < n; i++) {
                let boneIndex = this.readInt(!![]);
                let boneMap = {};
                for (let ii = 0x0, nn = this.readInt(!![]); ii < nn; ii++) {
                    const timelineType = this.readByte();
                    const frameCount = this.readInt(!![]);
                    let timeline;
                    let timelineScale = 0x1;

                    switch (timelineType) {
                        case 0x0:
                            timeline = new Array(frameCount);
                            for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                                const time = this.readFloat();
                                const tlangle = this.readFloat();
                                timeline[frameIndex] = {};
                                timeline[frameIndex].time = time;
                                timeline[frameIndex].angle = tlangle;
                                if (frameIndex < frameCount - 0x1) {
                                    this.readCurve(frameIndex, timeline);
                                }
                            }
                            boneMap.rotate = timeline;
                            duration = Math.max(duration, timeline[frameCount - 0x1].time);
                            break;
                        case 0x1:
                        case 0x2:
                        case 0x3:
                            timeline = new Array(frameCount);
                            if (timelineType == 0x1) {
                                timelineScale = scale;
                            }
                            for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                                let tltime = this.readFloat();
                                let tlx = this.readFloat();
                                let tly = this.readFloat();
                                timeline[frameIndex] = {};
                                timeline[frameIndex].time = tltime;
                                timeline[frameIndex]["x"] = tlx * timelineScale;
                                timeline[frameIndex]["y"] = tly * timelineScale;
                                if (frameIndex < frameCount - 0x1) {
                                    this.readCurve(frameIndex, timeline);
                                }
                            }
                            if (timelineType == 0x1) {
                                boneMap.translate = timeline;
                            } else if (timelineType == 0x2) {
                                boneMap.scale = timeline;
                            } else {
                                boneMap.shear = timeline;
                            }
                            duration = Math.max(duration, timeline[frameCount - 0x1].time);
                            break;
                    }
                }
                bones[this.json.bones[boneIndex].name] = boneMap;
            }
            animation.bones = bones;
            let ik = {};
            for (let i = 0x0, nn = this.readInt(!![]); i < nn; i++) {
                const ikIndex = this.readInt(!![]);
                const frameCount = this.readInt(!![]);
                const timeline = new Array(frameCount);
                for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                    const time = this.readFloat();
                    const mix = this.readFloat();
                    const bendPositive = this.readByte() != 0xff;
                    timeline[frameIndex] = {};
                    timeline[frameIndex].time = time;
                    timeline[frameIndex].mix = mix;
                    timeline[frameIndex].bendPositive = bendPositive;
                    if (frameIndex < frameCount - 0x1) {
                        this.readCurve(frameIndex, timeline);
                    }
                }
                ik[this.json["ik"][ikIndex].name] = timeline;
                duration = Math.max(duration, timeline[frameCount - 0x1].time);
            }
            animation["ik"] = ik;
            let ffd = {};
            for (let i = 0x0, nn = this.readInt(!![]); i < nn; i++) {
                const slotIndex = this.readInt(!![]);
                const frameCount = this.readInt(!![]);
                const timeline = new Array(frameCount);
                for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                    timeline[frameIndex] = {};
                    timeline[frameIndex].time = this.readFloat();
                    timeline[frameIndex].rotateMix = this.readFloat();
                    timeline[frameIndex].translateMix = this.readFloat();
                    timeline[frameIndex].scaleMix = this.readFloat();
                    timeline[frameIndex].shearMix = this.readFloat();
                    if (frameIndex < frameCount - 0x1) {
                        this.readCurve(frameIndex, timeline);
                    }
                }
                ffd[this.json.transform[slotIndex].name] = timeline;
                duration = Math.max(duration, timeline[frameCount - 0x1].time);
            }
            animation.transform = ffd;

            let path = {};
            for (let i = 0x0, n = this.readInt(!![]); i < n; i++) {
                let pathOrder = this.readInt(!![]);
                let pathData = this.json.path[pathOrder];
                let data = {};
                for (let ii = 0x0, nn = this.readInt(!![]); ii < nn; ii++) {
                    const timelineType = this.readByte();
                    const frameCount = this.readInt(!![]);
                    let timeline, time, timelineScale;
                    switch (timelineType) {
                        case 0x0:
                        case 0x1:
                            timeline = new Array(frameCount);
                            timelineScale = 0x1;
                            if (timelineType == 0x1) {
                                if (
                                    pathData.spacingMode == "length" ||
                                    pathData.spacingMode == "fixed"
                                ) {
                                    timelineScale = this.scale;
                                }
                            } else {
                                if (pathData.positionMode == "fixed") {
                                    timelineScale = this.scale;
                                }
                            }
                            for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                                time = this.readFloat();
                                let _0xebc795 = this.readFloat();
                                timeline[frameIndex] = {};
                                timeline[frameIndex].time = time;
                                if (timelineType == 0x0) {
                                    timeline[frameIndex].position = _0xebc795 * timelineScale;
                                } else {
                                    timeline[frameIndex].spacing = _0xebc795 * timelineScale;
                                }
                                if (frameIndex < frameCount - 0x1)
                                    this.readCurve(frameIndex, timeline);
                            }
                            if (timelineType == 0x0) {
                                data.position = timeline;
                            } else {
                                data.spacing = timeline;
                            }
                            duration = Math.max(duration, timeline[frameCount - 0x1].time);
                            break;
                        case 0x2:
                            timeline = new Array(frameCount);
                            for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                                time = this.readFloat();
                                let _0x47f0f0 = this.readFloat();
                                let _0x1bb1d7 = this.readFloat();
                                timeline[frameIndex] = {};
                                timeline[frameIndex].time = time;
                                timeline[frameIndex].rotateMix = _0x47f0f0;
                                timeline[frameIndex].translateMix = _0x1bb1d7;
                                if (frameIndex < frameCount - 0x1)
                                    this.readCurve(frameIndex, timeline);
                            }
                            data.mix = timeline;
                            duration = Math.max(duration, timeline[frameCount - 0x1].time);
                            break;
                    }
                }
                path[this.json.path[pathOrder].name] = data;
            }
            animation.paths = path;

            // 变量名可能不对
            let deform = {};
            for (let i = 0x0, n = this.readInt(!![]); i < n; i++) {
                let index = this.readInt(!![]);
                let skinName = this.json.skinsName[index];
                let deformData = {};
                for (let ii = 0x0, nn = this.readInt(!![]); ii < nn; ii++) {
                    const slotIndex = this.readInt(!![]);
                    const slot = this.json.slots[slotIndex];
                    const attachment = {};
                    for (let iii = 0x0, nnn = this.readInt(!![]); iii < nnn; iii++) {
                        const name = this.readString();
                        const frameCount = this.readInt(!![]);
                        const timeline = new Array(frameCount);
                        for (let frameIndex = 0x0; frameIndex < frameCount; frameIndex++) {
                            const time = this.readFloat();
                            const end = this.readInt(!![]);
                            timeline[frameIndex] = {};
                            timeline[frameIndex].time = time;
                            if (end != 0x0) {
                                let vertices = new Array(end);
                                let start = this.readInt(!![]);
                                if (this.scale == 0x1) {
                                    for (let i = 0x0; i < end; i++) {
                                        vertices[i] = this.readFloat();
                                    }
                                } else {
                                    for (let i = 0x0; i < end; i++) {
                                        vertices[i] = this.readFloat() * this.scale;
                                    }
                                }
                                timeline[frameIndex].offset = start;
                                timeline[frameIndex].vertices = vertices;
                            }
                            if (frameIndex < frameCount - 0x1)
                                this.readCurve(frameIndex, timeline);
                        }
                        attachment[name] = timeline;
                        duration = Math.max(duration, timeline[frameCount - 0x1].time);
                    }
                    deformData[slot.name] = attachment;
                }
                deform[skinName] = deformData;
            }
            animation.deform = deform;

            let drawOrderCount = this.readInt(!![]);
            if (drawOrderCount > 0x0) {
                let drawOrders = new Array(drawOrderCount);
                for (let i = 0x0; i < drawOrderCount; i++) {
                    const drawOrderMap = {};
                    const time = this.readFloat();
                    const offsetCount = this.readInt(!![]);
                    const offsets = new Array(offsetCount);
                    for (let ii = 0x0; ii < offsetCount; ii++) {
                        const offsetMap = {};
                        const slotIndex = this.readInt(!![]);
                        offsetMap.slot = this.json.slots[slotIndex].name;
                        let dooffset = this.readInt(!![]);
                        offsetMap.offset = dooffset;
                        offsets[ii] = offsetMap;
                    }
                    drawOrderMap.offsets = offsets;
                    drawOrderMap.time = time;
                    drawOrders[i] = drawOrderMap;
                }
                duration = Math.max(duration, drawOrders[drawOrderCount - 0x1].time);
                animation.drawOrder = drawOrders;
            }
            let eventCount = this.readInt(!![]);
            if (eventCount > 0x0) {
                let events = new Array(eventCount);
                for (let i = 0x0; i < eventCount; i++) {
                    const time = this.readFloat();
                    const name = this.json.eventsName[this.readInt(!![])];
                    const eventData = this.json.events[name];
                    const e = {};
                    e.name = name;
                    e.int = this.readInt(![]);
                    e.float = this.readFloat();
                    e.string = this.readBoolean() ? this.readString() : eventData.string;
                    e.time = time;
                    events[i] = e;
                }
                duration = Math.max(duration, events[eventCount - 0x1].time);
                animation.events = events;
            }
            return animation;
        };
        return SkeletonJsonConverter;
    })();
})(spine);

控制spine代码

这部分是我们可以用来控制具体行为的代码,随便魔改hhh。

myspine.js

将以下代码保存为myspine.js,放在主题文件的源目录source/js/third-party/myspine路径下。

image-20211102201351068

function MySpine(t) {
    this.config = t,
    this.model = t.models[Number.parseInt(Math.random()*t.models.length)],
    this.urlPrefix = t.spineDir + this.model.name,
    this.skin = this.model.skin,
    this.skeleton = this.urlPrefix + this.model.skeleton,
    this.atlas = this.urlPrefix + this.model.atlas,
    this.widget = null,
    this.widgetContainer = document.querySelector(".myspine-spine-widget"),
    this.voiceText = document.createElement("div"),
    this.voicePlayer = new Audio,
    this.triggerEvents = ["mousedown", "touchstart", "scroll"],
    this.animationQueue = new Array,
    this.isPlayingVoice = !1,
    this.lastInteractTime = Date.now(),
    this.localX = 0,
    this.localY = 0,
    this.load()
}
MySpine.downloadBinary = function(t, e, i) {
    var n = new XMLHttpRequest;
    n.open("GET", t, !0),
    n.responseType = "arraybuffer",
    n.onload = function() {
        200 == n.status ? e(new Uint8Array(n.response)) : i(n.status, n.responseText)
    }
    ,
    n.onerror = function() {
        i(n.status, n.responseText)
    }
    ,
    n.send()
}
,
MySpine.prototype = {
    load: function() {
        let i = this.config;
        MySpine.downloadBinary(this.skeleton, t=>{
            function e(t, e) {
                for (var i in e)
                    t.style.setProperty(i, e[i])
            }
            e(this.widgetContainer, i.styles.widget),
            e(this.voiceText, i.styles.voiceText);
            t = new spine.SkeletonJsonConverter(t,1);
            t.convertToJson(),
            new spine.SpineWidget(this.widgetContainer,{
                animation: this.getAnimationList("start")[0].name,
                skin: this.skin,
                atlas: this.atlas,
                jsonContent: t.json,
                backgroundColor: "#00000000",
                loop: !1,
                success: this.spineWidgetSuccessCallback.bind(this)
            })
        }
        , function(t, e) {
            console.error(`Couldn't download skeleton ${path}: status ${t}, ${e}.`)
        })
    },
    spineWidgetSuccessCallback: function(t) {
        var e = ()=>{
            this.triggerEvents.forEach(t=>window.removeEventListener(t, e)),
            this.triggerEvents.forEach(t=>window.addEventListener(t, this.changeIdleAnimation.bind(this))),
            this.initVoiceComponents(),
            this.initWidgetActions(),
            this.initDragging(),
            this.widget.play(),
            this.playVoice(this.getVoice("start")),
            this.widgetContainer.style.display = "block"
        }
        ;
        this.widget = t,
        this.widget.pause(),
        this.widgetContainer.style.display = "none",
        this.triggerEvents.forEach(t=>window.addEventListener(t, e))
    },
    initVoiceComponents: function() {
        this.voiceText.setAttribute("class", "myspine-voice-text"),
        this.widgetContainer.appendChild(this.voiceText),
        this.voiceText.style.opacity = 0,
        this.voicePlayer.addEventListener("timeupdate", ()=>{
            this.voiceText.scrollTo({
                left: 0,
                top: this.voiceText.offsetHeight * (this.voicePlayer.currentTime / this.voicePlayer.duration),
                behavior: "smooth"
            })
        }
        ),
        this.voicePlayer.addEventListener("ended", ()=>{
            this.voiceText.style.opacity = 0,
            this.isPlayingVoice = !1
        }
        )
    },
    initWidgetActions: function() {
        this.widget.canvas.onclick = this.interact.bind(this),
        this.widget.state.addListener({
            complete: t=>{
                (this.isPlayingVoice && t.loop) || this.isIdle() ? this.playRandAnimation({
                    name: t.animation.name,
                    loop: !0
                }) : this.playRandAnimation(this.getAnimationList("idle"))
            }
        })
    },
    initDragging: function() {
        function i(t) {
            var e = document.documentElement.scrollLeft
              , i = document.documentElement.scrollTop;
            return t.targetTouches ? (e += t.targetTouches[0].clientX,
            i += t.targetTouches[0].clientY) : t.clientX && t.clientY && (e += t.clientX,
            i += t.clientY),
            {
                x: e,
                y: i
            }
        }
        function e(t) {
            t.cancelable && t.preventDefault()
        }
        var n = (t,e)=>{
            t = Math.max(0, t),
            e = Math.max(0, e),
            t = Math.min(document.body.clientWidth - this.widgetContainer.clientWidth, t),
            e = Math.min(document.body.clientHeight - this.widgetContainer.clientHeight, e),
            this.widgetContainer.style.left = t + "px",
            this.widgetContainer.style.top = e + "px"
        }
          , o = t=>{
            var {x: e, y: t} = i(t);
            this.localX = e - this.widgetContainer.offsetLeft,
            this.localY = t - this.widgetContainer.offsetTop
        }
          , s = t=>{
            var {x: e, y: t} = i(t);
            n(e - this.localX, t - this.localY),
            window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty()
        }
          , t = {
            passive: !0
        }
          , a = {
            passive: !1
        };
        this.widgetContainer.addEventListener("mousedown", t=>{
            o(t),
            document.addEventListener("mousemove", s)
        }
        ),
        this.widgetContainer.addEventListener("touchstart", t=>{
            o(t),
            document.addEventListener("touchmove", e, a)
        }
        , t),
        this.widgetContainer.addEventListener("touchmove", s, t),
        document.addEventListener("mouseup", ()=>document.removeEventListener("mousemove", s)),
        this.widgetContainer.addEventListener("touchend", ()=>document.removeEventListener("touchmove", e)),
        window.addEventListener("resize", ()=>{
            let t = this.widgetContainer.style;
            var e, i;
            t.left && t.top && (e = Number.parseInt(t.left.substring(0, t.left.length - 2)),
            i = Number.parseInt(t.top.substring(0, t.top.length - 2)),
            n(e, i))
        }
        )
    },
    interact: function() {
        this.isPlayingVoice || 0 < this.animationQueue.length || !this.isIdle() ? console.warn("互动过于频繁!") : (this.lastInteractTime = Date.now(),
        this.playRandAnimation(this.getAnimationList("interact")),
        this.playVoice(this.getVoice("interact")))
    },
    getUrl: function(t) {
        return this.urlPrefix + t
    },
    getAnimationList: function(t) {
        var e = this.config.behaviors[t];
        return "start" == t? [{
            name: e.animation,
            loop: !1
        }] : e.animations.slice()
    },
    getVoice: function(t) {
        var e = this.config.behaviors[t];
        return "start" == t? {
            voice: e.voice,
            text: e.text
        } : e.voices[Math.floor(Math.random() * e.voices.length)]
    },
    playRandAnimation: function(t) {
        if (Array.isArray(t)) {
            e = t[Number.parseInt(Math.random()*t.length)];
            this.widget.state.setAnimation(0, e.name, e.loop);
        } else {
            this.widget.state.setAnimation(0, t.name, t.loop);
        }
    },
    playVoice: function(t) {
        voiceUrl = this.getUrl(t.voice);
        // 如果为空则播放五秒的空语音,仅用来显示文本框
        if (voiceUrl == this.urlPrefix) {
            voiceUrl = this.config.spineDir + "default.mp3";
        }
        t && (this.isPlayingVoice = !0,
        this.voicePlayer.src = voiceUrl,
        this.voicePlayer.load(),
        this.voicePlayer.play().then(()=>{
            this.voiceText.innerHTML = t.text,
            this.voiceText.scrollTo(0, 0),
            this.voiceText.style.opacity = 1
        }
        , t=>{
            this.isPlayingVoice = !1,
            console.error(`无法播放音频,因为:${t}`)
        }
        ))
    },
    isIdle: function() {
        var e = this.widget.state.tracks[0].animation;
        for (const v of this.getAnimationList("idle")) {
            if (v.name == e.name) {
                return !0;
            }
        }
        return !1;
    },
    isInteract: function() {
        var e = this.widget.state.tracks[0].animation;
        for (const v of this.getAnimationList("interact")) {
            if (v.name == e.name) {
                return !0;
            }
        }
        return !1;
    },
    changeIdleAnimation: function() {
        var t = Date.now()
          , e = t - this.lastInteractTime;
        if ((this.isIdle() && e/1e3/60 >= this.config.behaviors.idle.maxMinutes) || 
            (this.isInteract() && e/1e3 >= this.config.behaviors.interact.maxPlaySec)) {
             this.lastInteractTime = t,
             this.playRandAnimation(this.getAnimationList("idle"))
        }
    }
};

myspine.css

将以下代码保存为myspine.css,放在主题文件的源目录source/css/_third-party路径下。

image-20211102201924645

.myspine-spine-widget {
    position: fixed;
    width: 200px;
    height: 200px;
    left: 10px;
    bottom: 10px;
    z-index: 999;
}

.myspine-voice-text {
    width: 100%;
    height: 60px;
    position: absolute;
    margin-top: -70px;
    padding: 5px 5px 5px 5px;
    line-height: 1.5;
    color: #fff;
    background-color: #14151685;
    box-shadow: 0 3px 15px 2px #141516;
    font-size: 12px;
    text-align: left;
    text-overflow: ellipsis;
    overflow-x: hidden;
    overflow-y: scroll;
    transition: opacity .3s ease-in-out,height .5s ease-in-out;
}
/*
.myspine-voice-text::before {
    content: 'VOICE TEXT';
    display: block;
    color: #37b2ff;
    font-weight: 700;
}*/

.myspine-voice-text {
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.myspine-voice-text::-webkit-scrollbar {
    display: none;
}

增加调用

这一部分应该是Melody独有的,不知道别的主题有没有,不import的话css导不进去(

css调用

主题文件的源目录source/css/index.styl中加一句:

@import "_third-party/myspine.css"

image-20211102202119076

And...

在参考文章(在博客中添加明日方舟小人 - Stalo Fantasia♪)基础上的improve:

  1. 模型数量扩展,可使用多个模型。
  2. 模型动作展示从顺序切换更改为随机切换,增加互动乐趣。
  3. Idle动作和语音扩展,可用多个动作和语音,并增加到达一定时长随机切换的功能。
  4. 对interact动作进行时间限制,防止某些动作后摇过长。
  5. 调整部分CSS样式。

一些TODO:

  1. 砍语音部分(感觉容易社死,下个版本全盘砍掉)。
  2. 将语音文本独立出来做展示文字,可以多项文本随机切换。

如果想继续魔改的朋友们,把数据通路啥的梳理清楚了就很容易改啦~

posted @ 2022-01-26 13:42  c10udlnk  阅读(689)  评论(5编辑  收藏  举报