Commit 6e718fb5 authored by 李维's avatar 李维

翻页效果案例

parent a3f5ba9f
No preview for this file type
......@@ -57,25 +57,10 @@ npm start
* 下载模板调试专用app
安卓下载:
http://download-iplayabc.oss-cn-beijing.aliyuncs.com/iDebugABC.apk
![avatar](http://staging-teach.cdn.ireadabc.com/084f2f95-8213-4c5a-8c46-b194819d7677.png)
iOS下载:
由于调试APP没有上架App Store 所以需要先获取手机的UDID 发送给我们的技术支持,加入后才可以扫码下载安装
获取UDID:https://www.pgyer.com/tools/udid
下载iOS: https://www.pgyer.com/gS0X
有时可能需要反复调试一些功能性的问题,与原生APP相关或者手上临时没有设备,我们提供了一个网页版的调试方式
http://staging-openapi.iteachabc.com/api/courseware/v1/middle/debug
* 启动本地服务
```
......@@ -90,6 +75,5 @@ npm start
### 注意事项及常见问题
* 项目里所有文件及文件夹的命名方式要注意不能包含空格、汉字、减号
* 开发者新建的脚本文件(.js/.ts)的文件名必须包含项目名称,例如在 test_01 项目中添加一个脚本文件(如想命名为 hello.ts );则需要命名为 hello_test_01.ts
* 项目里尽量不要使用setTimeout、setInterval等定时器,如果使用了记得在onDestroy中释放掉(onDestroy 是指CocosCreator的生命周期钩子)
* 理论上禁止使用全局变量,因为模板到线上会进行组装,常见问题是一个模板使用多次造成全局变量被读脏
import { asyncDelay, onHomeworkFinish } from "../script/util";
import { MyCocosSceneComponent } from "../script/MyCocosSceneComponent";
import EventMgr from '../script/EventMgr'
const { ccclass, property } = cc._decorator;
@ccclass
export default class SceneComponent extends MyCocosSceneComponent {
@property({ type: cc.Node, displayName: "翻页容器" })
pageContainer: cc.Node = null
addPreloadImage() {
// TODO 根据自己的配置预加载图片资源
......@@ -21,11 +23,10 @@ export default class SceneComponent extends MyCocosSceneComponent {
}
async onLoadEnd() {
onLoadEnd() {
// TODO 加载完成后的逻辑写在这里, 下面的代码仅供参考
this.initData();
this.initView();
this.initListener();
}
_cantouch = null;
......@@ -38,7 +39,6 @@ export default class SceneComponent extends MyCocosSceneComponent {
this.initBg();
this.initPic();
this.initBtn();
this.initIcon();
}
initBg() {
......@@ -46,66 +46,91 @@ export default class SceneComponent extends MyCocosSceneComponent {
bgNode.scale = this._mapScaleMax;
}
pic1 = null;
pic2 = null;
currentPageIndex = 1;
captureJs = null;
currentFlipBeizerNode = null;
initPic() {
const canvas = cc.find('Canvas');
const maxW = canvas.width * 0.7;
this.getSprNodeByUrl(this.data.pic_url, (sprNode) => {
const picNode1 = sprNode;
picNode1.scale = maxW / picNode1.width;
picNode1.baseX = picNode1.x;
canvas.addChild(picNode1);
this.pic1 = picNode1;
const labelNode = new cc.Node();
labelNode.color = cc.Color.YELLOW;
const label = labelNode.addComponent(cc.Label);
label.string = this.data.text;
label.fontSize = 60;
label.lineHeight = 60;
label.font = cc.find('Canvas/res/font/BRLNSDB').getComponent('cc.Label').font;
picNode1.addChild(labelNode);
});
this.getSprNodeByUrl(this.data.pic_url_2, (sprNode) => {
const picNode2 = sprNode;
picNode2.scale = maxW / picNode2.width;
canvas.addChild(picNode2);
picNode2.x = canvas.width;
picNode2.baseX = picNode2.x;
this.pic2 = picNode2;
const labelNode = new cc.Node();
const label = labelNode.addComponent(cc.RichText);
const size = 60
label.font = cc.find('Canvas/res/font/BRLNSDB').getComponent(cc.Label).font;
label.string = `<outline color=#751e00 width=4><size=${size}><color=#ffffff>${this.data.text}</color></size></outline>`
label.lineHeight = size;
picNode2.addChild(labelNode);
});
const frame = cc.find('Canvas/pageFrame')
const page = cc.find('Canvas/page');
const canvas = cc.find('Canvas')
}
canvas.on("myMoveStart", ()=> {
this.flipPage(2,1);
})
canvas.on("myMoveEnd", ()=> {
if(this.currentFlipBeizerNode) {
this.currentFlipBeizerNode.destroy();
this.currentFlipBeizerNode = null;
}
})
initIcon() {
const iconNode = this.getSprNode('icon');
iconNode.zIndex = 5;
iconNode.anchorX = 1;
iconNode.anchorY = 1;
iconNode.parent = cc.find('Canvas');
iconNode.x = iconNode.parent.width / 2 - 10;
iconNode.y = iconNode.parent.height / 2 - 10;
iconNode.on(cc.Node.EventType.TOUCH_START, () => {
this.playAudioByUrl(this.data.audio_url);
canvas.on("myTouchMoving", (x)=> {
let p = ((canvas.width - x) * 180) / canvas.width
if(this.currentFlipBeizerNode) {
this.currentFlipBeizerNode.toAngle(p)
}
})
this.initBezierSize();
this.captureJs = this.pageContainer.getComponent("CaptureNative");
}
curPage = null;
initBtn() {
initBezierSize() {
const deviceWidth = cc.find("Canvas").width;
const deviceHeight = cc.find("Canvas").height;
const _page = cc.find('Canvas/page');
const mask = cc.find('mask', _page);
const bezier = cc.find('bezier', mask);
const page = cc.find('page', bezier);
_page.width = mask.width = bezier.width = page.width = deviceWidth;
_page.height = mask.height = bezier.height = page.height = deviceHeight;
}
setBezierContainer(node) {
const page = cc.find('Canvas/page');
// 把当前模板节点进行复制 用于创建多个页
let bezierNode = cc.instantiate(page);
// 找到书的页
const pic = cc.find('mask/bezier/page', bezierNode);
// 此处加载本地图片
const texture = node.getComponent(cc.Sprite).spriteFrame._texture;
// 把页码存在节点data中,在接收到翻页消息时判断哪页执行动画 - 在shader/scripts/page.ts中有用到
// bezierNode.data = {'page': i};
// 将翻页纹理上面的这一层放上图片、 背面的纹理在page/mask/bezier/page的属性检查器上可以设置
pic.getComponent('bezier-render').textureList[0] = texture;
pic.getComponent('bezier-render').textureList[1] = texture;
pic.getComponent('bezier-render').init();
const scriptPage = bezierNode.getComponent("page");
bezierNode.toAngle = (angle) => {
scriptPage.pageToAngle(angle);
}
// 显示
bezierNode.active = true;
return bezierNode;
}
async flipPage(index, nextIndex) {
const picNode = await this.captureJs.capture(this.pageContainer);
const bezierNode = this.setBezierContainer(picNode)
bezierNode.parent = cc.find("Canvas/BezierContainer");
const pc = cc.find("Canvas/pageFrame");
const len = pc.children.length;
pc.children[index].active = false;
pc.children[nextIndex].active = true;
this.currentFlipBeizerNode = bezierNode;
}
this.curPage = 0;
initBtn() {
const bottomPart = cc.find('Canvas/bottomPart');
bottomPart.zIndex = 5; // 提高层级
......@@ -118,13 +143,12 @@ export default class SceneComponent extends MyCocosSceneComponent {
if (!this._cantouch) {
return;
}
if (this.curPage == 0) {
if (this.currentPageIndex == 0) {
return;
}
this.curPage = 0
this.leftMove();
this.playLocalAudio('btn');
// 页码-1 需要让上一页执行从 左-右 动画
this.currentPageIndex--;
EventMgr.emit('page_to_right', this.currentPageIndex);
})
const rightBtnNode = bottomPart.getChildByName('btn_right');
......@@ -133,63 +157,22 @@ export default class SceneComponent extends MyCocosSceneComponent {
if (!this._cantouch) {
return;
}
if (this.curPage == 1) {
if (this.currentPageIndex == 5) {
return;
}
this.curPage = 1
this.rightMove();
// 游戏结束时需要调用这个方法通知系统作业完成
onHomeworkFinish();
this.playLocalAudio('btn');
// 当前页执行从 右-左 动画
EventMgr.emit('page_to_left', this.currentPageIndex)
this.currentPageIndex++;
})
}
leftMove() {
this._cantouch = false;
const len = this.pic1.parent.width;
cc.tween(this.pic1)
.to(1, { x: this.pic1.baseX }, { easing: 'cubicInOut' })
.start();
cc.tween(this.pic2)
.to(1, { x: this.pic2.baseX }, { easing: 'cubicInOut' })
.call(() => {
this._cantouch = true;
})
.start();
}
rightMove() {
this._cantouch = false;
const len = this.pic1.parent.width;
cc.tween(this.pic1)
.to(1, { x: this.pic1.baseX - len }, { easing: 'cubicInOut' })
.start();
cc.tween(this.pic2)
.to(1, { x: this.pic2.baseX - len }, { easing: 'cubicInOut' })
.call(() => {
this._cantouch = true;
})
.start();
}
// update (dt) {},
initListener() {
}
playLocalAudio(audioName) {
const audio = cc.find(`Canvas/res/audio/${audioName}`).getComponent(cc.AudioSource);
return new Promise((resolve, reject) => {
const id = cc.audioEngine.playEffect(audio.clip, false);
cc.audioEngine.setFinishCallback(id, () => {
resolve(id);
});
})
}
loadNetImg (url) {
return new Promise((resolve, reject) => {
cc.loader.load({ url }, (err, texture) => {
if (err && !texture) return resolve(pg.logger.w('loading loadRes warn-> ' + texture));
resolve(texture);
});
})
}
}
cc.Class({
extends: require('./textureRenderUtils'),
properties: {
_width: 0,
_height: 0
},
start () {
},
capture(drawPanelCenter, touchPointArr, id) {
this.init(drawPanelCenter);
// create the capture
this.camera.enabled = true;
return new Promise((resovle, reject) => {
this.schedule(() => {
let picData = this.initImage();
const picNode = this.createCanvas(picData);
picNode.id = id;
// this.label.string = 'Showing the capture'
// this.saveFile(picData);
this.camera.enabled = false;
// this.cleanTouchPointArr(touchPointArr);
// drawPanelCenter.addChild(picNode, 1);
if (this.drawLinePicCallback) {
this.drawLinePicCallback(picNode);
}
resovle(picNode)
}, 0.01, 0);
})
},
// override
initImage () {
let data = this.texture.readPixels();
this._width = this.texture.width;
this._height = this.texture.height;
let picData = this.filpYImage(data, this._width, this._height);
return picData;
},
// override init with Data
createCanvas (picData) {
let texture = new cc.Texture2D();
texture.initWithData(picData, 32, this._width, this._height);
let spriteFrame = new cc.SpriteFrame();
spriteFrame.setTexture(texture);
let node = new cc.Node();
let sprite = node.addComponent(cc.Sprite);
sprite.spriteFrame = spriteFrame;
return node;
node.zIndex = cc.macro.MAX_ZINDEX;
node.parent = cc.director.getScene();
// set position
let width = cc.winSize.width;
let height = cc.winSize.height;
node.x = width / 2;
node.y = height / 2;
// node.on(cc.Node.EventType.TOUCH_START, () => {
// node.parent = null;
// // this.label.string = '';
// node.destroy();
// });
// this.captureAction(node, width, height);
return node;
},
saveFile (picData) {
if (CC_JSB) {
let filePath = jsb.fileUtils.getWritablePath() + 'render_to_sprite_image.png';
let success = jsb.saveImageData(picData, this._width, this._height, filePath)
if (success) {
cc.log("save image data success, file: " + filePath);
}
else {
cc.error("save image data failed!");
}
}
},
// This is a temporary solution
filpYImage (data, width, height) {
// create the data array
let picData = new Uint8Array(width * height * 4);
let rowBytes = width * 4;
for (let row = 0; row < height; row++) {
let srow = height - 1 - row;
let start = srow * width * 4;
let reStart = row * width * 4;
// save the piexls data
for (let i = 0; i < rowBytes; i++) {
picData[reStart + i] = data[start + i];
}
}
return picData;
},
});
{
"ver": "1.0.8",
"uuid": "cc9fb885-c1f5-4927-977a-88ad75f3f14a",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
cc.Class({
extends: require('./textureRenderUtils'),
start () {
},
capture(drawPanelCenter, touchPointArr, id) {
this.init(drawPanelCenter);
// create capture
this.createCanvas();
var img = this.createImg();
return new Promise((resovle, reject) => {
this.scheduleOnce(() => {
const picNode = this.showImage(img);
picNode.id = id;
// this.cleanTouchPointArr(touchPointArr);
drawPanelCenter.addChild(picNode, 1);
if (this.drawLinePicCallback) {
this.drawLinePicCallback(picNode);
}
resovle(picNode)
}, 1);
})
},
// create the img element
createImg () {
// return the type and dataUrl
var dataURL = this._canvas.toDataURL("image/jpg");
var img = document.createElement("img");
img.src = dataURL;
return img;
},
clearCanvas () {
let ctx = this._canvas.getContext('2d');
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
},
// create the canvas and context, filpY the image Data
createCanvas () {
let width = this.texture.width;
let height = this.texture.height;
if (!this._canvas) {
this._canvas = document.createElement('canvas');
this._canvas.width = width;
this._canvas.height = height;
}
else {
this.clearCanvas();
}
let ctx = this._canvas.getContext('2d');
this.camera.render();
let data = this.texture.readPixels();
// write the render data
let rowBytes = width * 4;
for (let row = 0; row < height; row++) {
let srow = height - 1 - row;
let imageData = ctx.createImageData(width, 1);
let start = srow * width * 4;
for (let i = 0; i < rowBytes; i++) {
imageData.data[i] = data[start + i];
}
ctx.putImageData(imageData, 0, row);
}
return this._canvas;
},
});
\ No newline at end of file
{
"ver": "1.0.8",
"uuid": "08e55f30-848c-488a-aff0-a1b8eb9b9153",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
/**
* 事件模块
* onfire.js
*
*/
interface Listener {
cb: Function;
once: boolean;
target: object;
}
interface EventsType {
[eventName: string]: Listener[];
}
class EventMgr {
// 所有事件的监听器
es: EventsType = {};
on(eventName: string, cb: Function, target: object) {
if (!this.es[eventName]) {
this.es[eventName] = [];
}
this.es[eventName].push({
cb,
once: false,
target,
});
}
once(eventName: string, cb: Function, target: object) {
if (!this.es[eventName]) {
this.es[eventName] = [];
}
this.es[eventName].push({
cb,
once: true,
target,
});
}
emit(eventName: string, params?: any) {
const listeners = this.es[eventName] || [];
let l = listeners.length;
for (let i = 0; i < l; i++) {
const { cb, once, target } = listeners[i];
let args = [eventName, params];
cb.apply(target, args);
if (once) {
listeners.splice(i, 1);
i--;
l--;
}
}
}
off(eventName?: string, cb?: Function, target?: object) {
// clean all
if (eventName === undefined) {
this.es = {};
} else {
if (cb === undefined) {
// clean the eventName's listeners
delete this.es[eventName];
} else if (cb === null) {
if (!target) {
return;
}
const listeners = this.es[eventName] || [];
// clean the event and listener
let l = listeners.length;
for (let i = 0; i < l; i++) {
if (listeners[i].target === target) {
listeners.splice(i, 1);
i--;
l--;
}
}
} else {
const listeners = this.es[eventName] || [];
// clean the event and listener
let l = listeners.length;
for (let i = 0; i < l; i++) {
if (listeners[i].cb === cb && listeners[i].target === target) {
listeners.splice(i, 1);
i--;
l--;
}
}
}
}
}
}
export default new EventMgr();
\ No newline at end of file
{
"ver": "1.0.8",
"uuid": "fde6115d-4b2b-4000-9798-46e28c3ef1a7",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
// Learn cc.Class:
// - https://docs.cocos.com/creator/manual/en/scripting/class.html
// Learn Attribute:
// - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html
cc.Class({
extends: cc.Component,
properties: {
// foo: {
// // ATTRIBUTES:
// default: null, // The default value will be used only when the component attaching
// // to a node for the first time
// type: cc.SpriteFrame, // optional, default is typeof default
// serializable: true, // optional, default is true
// },
// bar: {
// get () {
// return this._bar;
// },
// set (value) {
// this._bar = value;
// }
// },
},
// LIFE-CYCLE CALLBACKS:
// onLoad () {},
start () {
const canvas = cc.find("Canvas")
this.node.width = canvas.width;
this.node.heigh = canvas.heigh;
},
// update (dt) {},
});
{
"ver": "1.0.8",
"uuid": "63ce9116-f0cb-4f31-9f06-095ac1056587",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
......@@ -87,7 +87,7 @@ export class MyCocosSceneComponent extends cc.Component {
preload() {
const preloadArr = this._imageResList.concat(this._audioResList).concat(this._animaResList);
cc.assetManager.preloadAny(preloadArr, null, null, async (err, data) => {
cc.assetManager.loadAny(preloadArr, null, null, (err, data) => {
if (window && window["air"]) {
// window["air"].onCourseInScreen = (next) => {
......@@ -95,10 +95,10 @@ export class MyCocosSceneComponent extends cc.Component {
// this.onLoadEnd();
// next();
// };
await this.onLoadEnd();
this.onLoadEnd();
window["air"].hideAirClassLoading();
} else {
await this.onLoadEnd();
this.onLoadEnd();
}
cc.debug.setDisplayStats(false);
......@@ -110,11 +110,11 @@ export class MyCocosSceneComponent extends cc.Component {
if(node){
node.getComponent('middleLayer').log(str);
}else{
console.log(str);
cc.log(str);
}
}
async onLoadEnd() {
onLoadEnd() {
}
......
// Learn cc.Class:
// - https://docs.cocos.com/creator/manual/en/scripting/class.html
// Learn Attribute:
// - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html
cc.Class({
extends: cc.Component,
properties: {
// foo: {
// // ATTRIBUTES:
// default: null, // The default value will be used only when the component attaching
// // to a node for the first time
// type: cc.SpriteFrame, // optional, default is typeof default
// serializable: true, // optional, default is true
// },
// bar: {
// get () {
// return this._bar;
// },
// set (value) {
// this._bar = value;
// }
// },
},
// LIFE-CYCLE CALLBACKS:
// onLoad () {},
start () {
this.moveDis = 0;
this.startX = 0;
this.isMoving = false;
this.node.on(cc.Node.EventType.TOUCH_START, (e)=>{
this.startX = e.getLocationX();
})
this.node.on(cc.Node.EventType.TOUCH_MOVE, (e)=>{
if(!this.isMoving && Math.abs(this.startX - e.getLocationX()) > 50) {
this.node.emit("myMoveStart", this.startX);
this.isMoving = true;
}
this.node.emit("myTouchMoving", e.getLocationX())
})
this.node.on(cc.Node.EventType.TOUCH_END, (e)=>{
this.isMoving = false;
this.node.emit("myMoveEnd");
})
},
// update (dt) {},
});
{
"ver": "1.0.8",
"uuid": "ea51b6de-5133-4ca8-8e4a-28f8277cc676",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
cc.Class({
extends: cc.Component,
properties: {
camera: cc.Camera,
// _canvas: null
},
init (drawPanelCenter) {
this.drawPanelCenter = drawPanelCenter;
const box = this.drawPanelCenter.box;
const canvas = cc.find("Canvas");
if (!this.camera) {
this.camera = this.node.addComponent(cc.Camera);
}
// this.camera.alignWithScreen = false;
// this.camera.orthoSize = 400;
// this.camera.backgroundColor = new cc.Color(0, 0, 0, 0);
// console.log(" this.drawPanelCenter.box: ", this.drawPanelCenter.box);
let texture = new cc.RenderTexture();
// texture.setPremultiplyAlpha(true);
texture.initWithSize( canvas.width, canvas.height, cc.RenderTexture.DepthStencilFormat.RB_FMT_S8);
this.camera.targetTexture = texture;
this.texture = texture;
},
// show on the canvas
showImage (img) {
// console.log('img: ', img);
let texture = new cc.Texture2D();
texture.initWithElement(img);
texture.setPremultiplyAlpha(true);
let spriteFrame = new cc.SpriteFrame();
spriteFrame.setTexture(texture);
console.log("spriteFrame.getRect(): ", spriteFrame.getRect());
// spriteFrame.setRect( this.drawPanelCenter.box );
let node = new cc.Node();
let sprite = node.addComponent(cc.Sprite);
sprite.spriteFrame = spriteFrame;
// node.x += 100;
// sprite.srcBlendFactor = cc.macro.BlendFactor.ONE;
// const box = this.drawPanelCenter.box
// const rect = new cc.Rect(box.x +3.5, box.y + 70.5, box.width, box.height)
// spriteFrame.setRect( rect );
// this.drawPanelCenter.addChild(node, 1);
// if (this.drawLinePicCallback) {
// this.drawLinePicCallback(node);
// }
return node;
node.zIndex = cc.macro.MAX_ZINDEX;
node.parent = cc.director.getScene();
// set position
let width = cc.winSize.width;
let height = cc.winSize.height;
node.x = width / 2;
node.y = height / 2;
// node.on(cc.Node.EventType.TOUCH_START, () => {
// node.parent = null;
// node.destroy();
// });
// this.captureAction(node, width, height);
},
// sprite action
captureAction (capture, width, height) {
let scaleAction = cc.scaleTo(1,0.3);
let targetPos = cc.v2(width - width / 6, height / 4);
let moveAction = cc.moveTo(1, targetPos);
let spawn = cc.spawn(scaleAction, moveAction);
capture.runAction(spawn);
let blinkAction = cc.blink(0.1, 1);
// scene action
this.node.runAction(blinkAction);
},
cleanTouchPointArr(touchPointArr) {
for (let i=0; i<touchPointArr.length; i++) {
if (touchPointArr[i]) {
touchPointArr[i].removeFromParent();
}
}
},
});
{
"ver": "1.0.8",
"uuid": "1d08bca4-c29b-4c49-8652-ef1cdc6301fa",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
......@@ -464,12 +464,3 @@ export function callMiddleLayerFunction(apiName: string, data: any, callback: Fu
}
}
export function showTips(tips) {
const middleLayer = cc.find('middleLayer');
if (middleLayer) {
middleLayer.getComponent('middleLayer').showTips(tips);
} else {
console.log(tips);
}
}
{
"ver": "1.1.2",
"uuid": "c0ca186a-22c8-49be-89e8-7261be2225ef",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}
\ No newline at end of file
// Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.
CCEffect %{
techniques:
- passes:
- vert: vs
frag: fs
blendState:
targets:
- blend: true
rasterizerState:
cullMode: none
properties:
texture0: { value: white }
texture1: { value: white }
alphaThreshold: { value: 0.5 }
}%
CCProgram vs %{
precision highp float;
#include <cc-global>
#include <cc-local>
in vec3 a_position;
in vec4 a_color;
out vec4 v_color;
#if USE_TEXTURE
in vec2 a_uv0;
out vec2 v_uv0;
in float a_isFront;
out float v_isFront;
#endif
void main () {
vec4 pos = vec4(a_position, 1);
#if CC_USE_MODEL
pos = cc_matViewProj * cc_matWorld * pos;
#else
pos = cc_matViewProj * pos;
#endif
#if USE_TEXTURE
v_uv0 = a_uv0;
#endif
v_color = a_color;
v_isFront = a_isFront;
gl_Position = pos;
}
}%
CCProgram fs %{
precision highp float;
#include <alpha-test>
in vec4 v_color;
#if USE_TEXTURE
in vec2 v_uv0;
in float v_isFront;
uniform sampler2D texture0;
uniform sampler2D texture1;
#endif
void main () {
vec4 o = vec4(1, 1, 1, 1);
#if USE_TEXTURE
if(v_isFront == 1.0) {
o *= texture(texture0, v_uv0);
} else {
o *= texture(texture1, v_uv0);
}
#if CC_USE_ALPHA_ATLAS_TEXTURE
o.a *= texture2D(texture, v_uv0).r;
#endif
#endif
o *= v_color;
ALPHA_TEST(o);
gl_FragColor = o;
}
}%
{
"ver": "1.0.25",
"uuid": "3f92d3ef-4697-48ef-8287-0ed8637b7257",
"compiledShaders": [
{
"glsl1": {
"vert": "\nprecision highp float;\nuniform mat4 cc_matViewProj;\nuniform mat4 cc_matWorld;\nattribute vec3 a_position;\nattribute vec4 a_color;\nvarying vec4 v_color;\n#if USE_TEXTURE\nattribute vec2 a_uv0;\nvarying vec2 v_uv0;\nattribute float a_isFront;\nvarying float v_isFront;\n#endif\nvoid main () {\n vec4 pos = vec4(a_position, 1);\n #if CC_USE_MODEL\n pos = cc_matViewProj * cc_matWorld * pos;\n #else\n pos = cc_matViewProj * pos;\n #endif\n #if USE_TEXTURE\n v_uv0 = a_uv0;\n #endif\n v_color = a_color;\n v_isFront = a_isFront;\n gl_Position = pos;\n}",
"frag": "\nprecision highp float;\n#if USE_ALPHA_TEST\n uniform float alphaThreshold;\n#endif\nvoid ALPHA_TEST (in vec4 color) {\n #if USE_ALPHA_TEST\n if (color.a < alphaThreshold) discard;\n #endif\n}\nvoid ALPHA_TEST (in float alpha) {\n #if USE_ALPHA_TEST\n if (alpha < alphaThreshold) discard;\n #endif\n}\nvarying vec4 v_color;\n#if USE_TEXTURE\nvarying vec2 v_uv0;\nvarying float v_isFront;\nuniform sampler2D texture0;\nuniform sampler2D texture1;\n#endif\nvoid main () {\n vec4 o = vec4(1, 1, 1, 1);\n #if USE_TEXTURE\n if(v_isFront == 1.0) {\n o *= texture2D(texture0, v_uv0);\n } else {\n o *= texture2D(texture1, v_uv0);\n }\n #if CC_USE_ALPHA_ATLAS_TEXTURE\n o.a *= texture2D(texture, v_uv0).r;\n #endif\n #endif\n o *= v_color;\n ALPHA_TEST(o);\n gl_FragColor = o;\n}"
},
"glsl3": {
"vert": "\nprecision highp float;\nuniform CCGlobal {\n mat4 cc_matView;\n mat4 cc_matViewInv;\n mat4 cc_matProj;\n mat4 cc_matProjInv;\n mat4 cc_matViewProj;\n mat4 cc_matViewProjInv;\n vec4 cc_cameraPos;\n vec4 cc_time;\n mediump vec4 cc_screenSize;\n mediump vec4 cc_screenScale;\n};\nuniform CCLocal {\n mat4 cc_matWorld;\n mat4 cc_matWorldIT;\n};\nin vec3 a_position;\nin vec4 a_color;\nout vec4 v_color;\n#if USE_TEXTURE\nin vec2 a_uv0;\nout vec2 v_uv0;\nin float a_isFront;\nout float v_isFront;\n#endif\nvoid main () {\n vec4 pos = vec4(a_position, 1);\n #if CC_USE_MODEL\n pos = cc_matViewProj * cc_matWorld * pos;\n #else\n pos = cc_matViewProj * pos;\n #endif\n #if USE_TEXTURE\n v_uv0 = a_uv0;\n #endif\n v_color = a_color;\n v_isFront = a_isFront;\n gl_Position = pos;\n}",
"frag": "\nprecision highp float;\n#if USE_ALPHA_TEST\n uniform ALPHA_TEST {\n float alphaThreshold;\n };\n#endif\nvoid ALPHA_TEST (in vec4 color) {\n #if USE_ALPHA_TEST\n if (color.a < alphaThreshold) discard;\n #endif\n}\nvoid ALPHA_TEST (in float alpha) {\n #if USE_ALPHA_TEST\n if (alpha < alphaThreshold) discard;\n #endif\n}\nin vec4 v_color;\n#if USE_TEXTURE\nin vec2 v_uv0;\nin float v_isFront;\nuniform sampler2D texture0;\nuniform sampler2D texture1;\n#endif\nvoid main () {\n vec4 o = vec4(1, 1, 1, 1);\n #if USE_TEXTURE\n if(v_isFront == 1.0) {\n o *= texture(texture0, v_uv0);\n } else {\n o *= texture(texture1, v_uv0);\n }\n #if CC_USE_ALPHA_ATLAS_TEXTURE\n o.a *= texture2D(texture, v_uv0).r;\n #endif\n #endif\n o *= v_color;\n ALPHA_TEST(o);\n gl_FragColor = o;\n}"
}
}
],
"subMetas": {}
}
\ No newline at end of file
{
"__type__": "cc.Material",
"_name": "page-material",
"_objFlags": 0,
"_native": "",
"_effectAsset": {
"__uuid__": "3f92d3ef-4697-48ef-8287-0ed8637b7257"
},
"_techniqueIndex": 0,
"_techniqueData": {
"0": {
"defines": {
"USE_TEXTURE": true
}
}
}
}
\ No newline at end of file
{
"ver": "1.0.3",
"uuid": "fa19c07e-e17f-428c-a3cd-3553530d5c1c",
"dataAsSubAsset": null,
"subMetas": {}
}
\ No newline at end of file
[
{
"__type__": "cc.Prefab",
"_name": "",
"_objFlags": 0,
"_native": "",
"data": {
"__id__": 1
},
"optimizationPolicy": 0,
"asyncLoadAssets": false,
"readonly": false
},
{
"__type__": "cc.Node",
"_name": "page",
"_objFlags": 0,
"_parent": null,
"_children": [
{
"__id__": 2
}
],
"_active": true,
"_components": [
{
"__id__": 11
}
],
"_prefab": {
"__id__": 12
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 0,
"height": 0
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "mask",
"_objFlags": 0,
"_parent": {
"__id__": 1
},
"_children": [
{
"__id__": 3
}
],
"_active": true,
"_components": [
{
"__id__": 9
}
],
"_prefab": {
"__id__": 10
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1279,
"height": 913
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "bezier",
"_objFlags": 0,
"_parent": {
"__id__": 2
},
"_children": [
{
"__id__": 4
}
],
"_active": true,
"_components": [
{
"__id__": 7
}
],
"_prefab": {
"__id__": 8
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1279,
"height": 913
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
0,
0,
0,
0,
1,
1,
1,
0.7
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "cc.Node",
"_name": "page",
"_objFlags": 0,
"_parent": {
"__id__": 3
},
"_children": [],
"_active": true,
"_components": [
{
"__id__": 5
}
],
"_prefab": {
"__id__": 6
},
"_opacity": 255,
"_color": {
"__type__": "cc.Color",
"r": 255,
"g": 255,
"b": 255,
"a": 255
},
"_contentSize": {
"__type__": "cc.Size",
"width": 1279,
"height": 913
},
"_anchorPoint": {
"__type__": "cc.Vec2",
"x": 0.5,
"y": 0.5
},
"_trs": {
"__type__": "TypedArray",
"ctor": "Float64Array",
"array": [
0,
0,
0,
0,
0,
0,
1,
1,
1,
1
]
},
"_eulerAngles": {
"__type__": "cc.Vec3",
"x": 0,
"y": 0,
"z": 0
},
"_skewX": 0,
"_skewY": 0,
"_is3DNode": false,
"_groupIndex": 0,
"groupIndex": 0,
"_id": ""
},
{
"__type__": "5293bOoH+xLvpF8qZYhUWKw",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 4
},
"_enabled": true,
"_materials": [
{
"__uuid__": "fa19c07e-e17f-428c-a3cd-3553530d5c1c"
}
],
"textureList": [
{
"__uuid__": "cabced38-3e05-4e9f-82db-731b9e7a868a"
},
{
"__uuid__": "cabced38-3e05-4e9f-82db-731b9e7a868a"
}
],
"pointsCount": 10,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "480a2cac-787a-432e-9a7e-c1e3ac0c5565"
},
"fileId": "7fTwAqo0NG6Lqv4FXc7gnu",
"sync": false
},
{
"__type__": "cc.Widget",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 3
},
"_enabled": true,
"alignMode": 1,
"_target": null,
"_alignFlags": 8,
"_left": 0,
"_right": 0,
"_top": 0,
"_bottom": 0,
"_verticalCenter": 0,
"_horizontalCenter": 0,
"_isAbsLeft": true,
"_isAbsRight": true,
"_isAbsTop": true,
"_isAbsBottom": true,
"_isAbsHorizontalCenter": true,
"_isAbsVerticalCenter": true,
"_originalWidth": 0,
"_originalHeight": 0,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "480a2cac-787a-432e-9a7e-c1e3ac0c5565"
},
"fileId": "07WXfgQGVKrJWb+EE/WbO7",
"sync": false
},
{
"__type__": "cc.Mask",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 2
},
"_enabled": true,
"_materials": [
{
"__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432"
}
],
"_spriteFrame": null,
"_type": 0,
"_segments": 64,
"_N$alphaThreshold": 0.1,
"_N$inverted": false,
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "480a2cac-787a-432e-9a7e-c1e3ac0c5565"
},
"fileId": "61FFlT3+ZNkoeK01icAIXP",
"sync": false
},
{
"__type__": "bcce1R81+lF5LYV8yzve2zA",
"_name": "",
"_objFlags": 0,
"node": {
"__id__": 1
},
"_enabled": true,
"page": {
"__id__": 4
},
"bezierRender": {
"__id__": 5
},
"_id": ""
},
{
"__type__": "cc.PrefabInfo",
"root": {
"__id__": 1
},
"asset": {
"__uuid__": "480a2cac-787a-432e-9a7e-c1e3ac0c5565"
},
"fileId": "",
"sync": false
}
]
\ No newline at end of file
{
"ver": "1.2.9",
"uuid": "480a2cac-787a-432e-9a7e-c1e3ac0c5565",
"optimizationPolicy": "AUTO",
"asyncLoadAssets": false,
"readonly": false,
"subMetas": {}
}
\ No newline at end of file
{
"ver": "1.1.2",
"uuid": "f7d3cbad-a530-4ad3-8eb5-9f53b0bfc261",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}
\ No newline at end of file
import PageEffectAssemblerBase from "./page-effect-assembler-base";
const gfx = cc.gfx
let vfmtPosUvColorFront = new gfx.VertexFormat([
{ name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
{ name: gfx.ATTR_UV0, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
{ name: gfx.ATTR_COLOR, type: gfx.ATTR_TYPE_UINT8, num: 4, normalize: true },
{ name: "a_isFront", type: gfx.ATTR_TYPE_FLOAT32, num: 1},
]);
export default class BezierAssembler extends PageEffectAssemblerBase {
protected angle:number = 0
public updateRenderData (comp: any) {
if (comp) {
let pointNum: number = comp.getPointCount()
if (pointNum < 2) {
return
}
let node = comp.node
let height = node.height
let width = node.width
// 左下角的坐标
let posX = - width * node.anchorX
let posY = - height * node.anchorY
// 根据角度获得控制点的位置
let ctrlPosData = this._getCtrlPosByAngle(width)
let startPos = ctrlPosData.startPos
let endPos = ctrlPosData.endPos
let ctrlPos1 = ctrlPosData.ctrlPos1
let ctrlPos2 = ctrlPosData.ctrlPos2
// 记录各个顶点的位置
let bezierPosList: cc.Vec2[] = []
bezierPosList[0] = startPos
// 当前所有顶点连线的总长
let realWidth = 0
// 上一个点的纹理坐标
let lastU = 0
// 下一个点的纹理坐标
let nextU = 0
let floatsPerVert = this.floatsPerVert;
let verts = this.renderData.vDatas[0];
// 写verts时的下标
let dstOffset = 0;
for (let i = 1; i < pointNum; i++) {
let isTail = i === pointNum - 1
let lastBezierPos = bezierPosList[i - 1]
let nextBezierPos = this._getBezierPos(i / (pointNum - 1) , startPos, endPos, ctrlPos1, ctrlPos2)
let fixedData = this._fixWidth(lastBezierPos, nextBezierPos, width, realWidth, isTail)
let gapWidth = fixedData.gapWidth
nextBezierPos = fixedData.nextBezierPos
realWidth += gapWidth
bezierPosList[i] = nextBezierPos
// 根据当前小矩形的宽度占总长度的比例来计算纹理坐标的间隔
let gapU = gapWidth / width
nextU = lastU + gapU
/*
分别计算小矩形四个顶点的坐标和纹理坐标
各顶点的坐标计算方法为在左下角坐标的基础上加上顶点在贝塞尔曲线上的坐标,如果是书页顶部的顶点则还要加上书页的高度
*/
// 将4个顶点数据写入verts
dstOffset = floatsPerVert * (i-1) * 4;
verts[dstOffset] = posX + lastBezierPos.x;
verts[dstOffset + 1] = posY + lastBezierPos.y;
verts[dstOffset + 2] = lastU;
verts[dstOffset + 3] = 1;
dstOffset += floatsPerVert;
verts[dstOffset] = posX + nextBezierPos.x;
verts[dstOffset + 1] = posY + nextBezierPos.y;
verts[dstOffset + 2] = nextU;
verts[dstOffset + 3] = 1;
dstOffset += floatsPerVert;
verts[dstOffset] = posX + lastBezierPos.x;
verts[dstOffset + 1] = posY + height + lastBezierPos.y;
verts[dstOffset + 2] = lastU;
verts[dstOffset + 3] = 0;
dstOffset += floatsPerVert;
verts[dstOffset] = posX + nextBezierPos.x;
verts[dstOffset + 1] = posY + height + nextBezierPos.y;
verts[dstOffset + 2] = nextU;
verts[dstOffset + 3] = 0;
lastU = nextU
}
this.updateColor(comp, null);
this.updateIsFront(comp, 5);
}
}
init(comp: cc.RenderComponent) {
super.init(comp);
//@ts-ignore
let segmentCount = comp.getPointCount() - 1;
this.verticesCount = 4 * segmentCount;
this.indicesCount = 6 * segmentCount;
this.floatsPerVert = 6;
this.initData();
}
getVfmt() {
return vfmtPosUvColorFront;
}
private _getCtrlPosByAngle(width: number): {startPos: cc.Vec2, endPos: cc.Vec2, ctrlPos1: cc.Vec2, ctrlPos2: cc.Vec2} {
let startPos = new cc.Vec2(0, 0)
let endPos = null
let ctrlPos1 = null
let ctrlPos2 = null
let rad = this.angle * Math.PI / 180
let per = rad * 2 / Math.PI
if(this.angle <= 90) {
// 终点的x坐标变换 width => 0,速度先慢后快,使用InCubic缓动函数
let endPosX = width * (1 - Math.pow(per, 3))
// InCubic
// 终点的y坐标变换 0 => width / 4, 速度先快后慢,使用OutQuart缓动函数
let endPosY = width / 4 * (1 - Math.pow(1 - per, 4))
endPos = new cc.Vec2(endPosX, endPosY)
// 中间两个控制点坐标匀速变换
// x坐标 width => width * 3 / 4
let ctrlPosX = width * (1 - 1 / 4 * per)
// 控制点1y坐标 0 => width / 16
let ctrlPos1Y = width * 1 / 16 * per
// 控制点2y坐标 0 => width * 3 / 16
let ctrlPos2Y = width * 3 / 16 * per
ctrlPos1 = new cc.Vec2(ctrlPosX, ctrlPos1Y)
ctrlPos2 = new cc.Vec2(ctrlPosX, ctrlPos2Y)
} else {
per = per - 1
// 终点的x坐标变换 0 => width,速度先快后慢,使用OutCubic缓动函数
let endPosX = - width * (1 - Math.pow(1 - per, 3))
// 终点的y坐标变换 width / 4 => 0, 速度先慢后快,使用InQuart缓动函数
let endPosY = width / 4 * (1 - Math.pow(per, 4))
endPos = new cc.Vec2(endPosX, endPosY)
// 控制点1x坐标 width * 3 / 4 => 0
let ctrlPos1X = width * 3 / 4 * (1 - per)
// 控制点2x坐标 width * 3 / 4 => 0
let ctrlPos2X = width * 3 / 4 * Math.pow(1 - per, 3)
// 控制点1y坐标 width / 16 => 0
let ctrlPos1Y = width * 1 / 16 * (1 - per)
// 控制点2y坐标 width * 3 / 16 => 0
let ctrlPos2Y = width * 3 / 16 * (1 - Math.pow(per, 4))
ctrlPos1 = new cc.Vec2(ctrlPos1X, ctrlPos1Y)
ctrlPos2 = new cc.Vec2(ctrlPos2X, ctrlPos2Y)
}
return {
startPos: startPos,
endPos: endPos,
ctrlPos1: ctrlPos1,
ctrlPos2: ctrlPos2
}
}
// 修正宽度
private _fixWidth(lastBezierPos: cc.Vec2, nextBezierPos: cc.Vec2, width: number, realWidth: number, isTail: boolean) {
let deltaVector = nextBezierPos.sub(lastBezierPos)
// 两个顶点的间距
let gapWidth = deltaVector.mag()
// 当前的总长
let curWidth = realWidth + gapWidth
if(isTail) {
// 如果是最后一个顶点则将总长度修正至书页的真实宽度
gapWidth = width - realWidth
let direction = deltaVector.normalize()
nextBezierPos = lastBezierPos.add(direction.mul(gapWidth))
} else if(curWidth >= width) {
// 如果当前总长超过了书页的真实宽度,就衰减超过部分的1.1倍
let delta = curWidth - width
gapWidth = gapWidth - delta * 1.1
gapWidth = Math.max(0, gapWidth)
let direction = deltaVector.normalize()
nextBezierPos = lastBezierPos.add(direction.mul(gapWidth))
}
return {
gapWidth: gapWidth,
nextBezierPos: nextBezierPos,
}
}
// 贝塞尔曲线公式
private _getBezierPos(t: number, startPos: cc.Vec2, endPos: cc.Vec2, ctrlPos1: cc.Vec2, ctrlPos2: cc.Vec2): cc.Vec2 {
startPos = startPos.mul(Math.pow(1 - t, 3))
ctrlPos1 = ctrlPos1.mul(3 * t * Math.pow(1 - t, 2))
ctrlPos2 = ctrlPos2.mul(3 * (1 - t) * Math.pow(t, 2))
endPos = endPos.mul(Math.pow(t, 3))
return startPos.add(ctrlPos1.add(ctrlPos2.add(endPos)))
}
}
\ No newline at end of file
{
"ver": "1.0.8",
"uuid": "894e28b7-e0eb-4fbc-9c92-71c92dc4322f",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
import BezierAssembler from "./bezier-assembler";
const { ccclass, property } = cc._decorator;
@ccclass
export default class BezierRender extends cc.RenderComponent {
@property({ type: [cc.Texture2D], displayName: "纹理" })
public textureList: cc.Texture2D[] = []
@property({ displayName: "每条边上的顶点数量" })
public pointsCount: number = 10
protected _initedMaterial: boolean = false
onEnable() {
super.onEnable();
// this.init();
}
public init() {
if (!this._initedMaterial) {
this.updateMaterial()
}
this.setVertsDirty()
}
public _resetAssembler() {
let assembler = this._assembler = new BezierAssembler()
assembler.init(this)
}
protected _updateMaterial() {
let material = this.getMaterial(0)
if (material) {
material.define('CC_USE_MODEL', 1);
if (this.textureList.length === 2) {
material.setProperty('texture0', this.textureList[0]);
material.setProperty('texture1', this.textureList[1]);
}
}
}
protected updateMaterial() {
if (this.textureList.length === 2) {
this._updateMaterial()
this._initedMaterial = true
return
}
}
public getPointCount() {
return this.pointsCount
}
public updateAngle(angle: number) {
if (!this._assembler) {
return
}
this._assembler.angle = angle;
this._assembler.updateRenderData(this);
}
}
\ No newline at end of file
{
"ver": "1.0.8",
"uuid": "5293b3a8-1fec-4bbe-917c-a996215162b0",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
export default class PageEffectAssemblerBase extends cc.Assembler {
// 普通四边形的属性,根据实际的顶点格式、数量调整
protected verticesCount = 4;
protected indicesCount = 6;
protected floatsPerVert = 5;
protected colorOffset = 4;
protected renderData: cc.RenderData = null;
get verticesFloats() {
return this.verticesCount * this.floatsPerVert;
}
getBuffer() {
//@ts-ignore
return cc.renderer._handle.getBuffer("mesh", this.getVfmt());
}
getVfmt() {
// to be overwrite
return null;
}
updateColor(comp, color) {
let uintVerts = this.renderData.uintVDatas[0];
if (!uintVerts) return;
color = color != null ? color : comp.node.color._val;
let floatsPerVert = this.floatsPerVert;
let colorOffset = this.colorOffset;
for (let i = colorOffset, l = uintVerts.length; i < l; i += floatsPerVert) {
uintVerts[i] = color;
}
}
updateIsFront(comp, dataOffset) {
let verts = this.renderData.vDatas[0];
let index = 0;
let floatsPerVert = this.floatsPerVert;
for (let i = 0, n = this.verticesCount; i < n; ++i) {
index = i * floatsPerVert;
let isFirstVert = i % 2 === 0;
let firstVertX = isFirstVert ? verts[index] : verts[index - floatsPerVert];
let secondVertX = isFirstVert ? verts[index + floatsPerVert] : verts[index];
let isFront = firstVertX < secondVertX ? 1.0 : 0.0;
verts[index + dataOffset] = isFront;
}
}
initData() {
//@ts-ignore
this.renderData = new cc.RenderData();
this.renderData.init(this);
let data = this.renderData;
// createFlexData支持创建指定格式的renderData
data.createFlexData(0, this.verticesCount, this.indicesCount, this.getVfmt());
// 顶点数固定的情况下索引不变化
let indices = data.iDatas[0];
let count = indices.length / 6;
for (let i = 0, idx = 0; i < count; i++) {
let vertextID = i * 4;
indices[idx++] = vertextID;
indices[idx++] = vertextID+1;
indices[idx++] = vertextID+2;
indices[idx++] = vertextID+1;
indices[idx++] = vertextID+3;
indices[idx++] = vertextID+2;
}
}
fillBuffers(comp, renderer) {
let renderData = this.renderData;
let vData = renderData.vDatas[0];
let iData = renderData.iDatas[0];
let buffer = this.getBuffer();
let offsetInfo = buffer.request(this.verticesCount, this.indicesCount);
let vertexOffset = offsetInfo.byteOffset >> 2,
vbuf = buffer._vData;
if (vData.length + vertexOffset > vbuf.length) {
vbuf.set(vData.subarray(0, vbuf.length - vertexOffset), vertexOffset);
} else {
vbuf.set(vData, vertexOffset);
}
let ibuf = buffer._iData,
indiceOffset = offsetInfo.indiceOffset,
vertexId = offsetInfo.vertexOffset;
for (let i = 0, l = iData.length; i < l; i++) {
ibuf[indiceOffset++] = vertexId + iData[i];
}
}
}
{
"ver": "1.0.8",
"uuid": "98829bbe-dd8d-4700-bd45-d2658ff9237b",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
import EventMgr from '../../script/EventMgr'
import BezierRender from "./bezier-render";
const { ccclass, property } = cc._decorator;
@ccclass
export default class Page extends cc.Component {
@property({ type: cc.Node, displayName: "图片节点" })
public page: cc.Node = null
@property({ type: BezierRender, displayName: "贝塞尔曲线" })
public bezierRender: BezierRender = null
// @property({ type: VerletRender, displayName: "Verlet积分算法" })
// public verletRender: VerletRender = null
onLoad() {
EventMgr.on("page_to_left", (msg, page) => {
console.log(this.node.data, page)
if(this.node.data && this.node.data.page == page) {
// 判断是否是当前实例进行翻页
this.pageToLeft();
}
}, null)
EventMgr.on("page_to_right", (msg, page) => {
if(this.node.data && this.node.data.page == page) {
// 判断是否是当前实例进行翻页
this.pageToRight();
}
}, null)
}
pageToAngle(angle) {
this.bezierRender.updateAngle(angle)
}
pageToLeft() {
let angle = 0;
let inter = setInterval(() => {
angle += 15;
if (angle > 180) {
angle = 180;
clearInterval(inter);
}
this.bezierRender.updateAngle(angle)
}, 30)
}
pageToRight() {
let angle = 180;
let inter = setInterval(() => {
angle -= 15;
if (angle < 0) {
angle = 0;
clearInterval(inter);
}
this.bezierRender.updateAngle(angle)
}, 30)
}
}
{
"ver": "1.0.8",
"uuid": "bcce147c-d7e9-45e4-b615-f32cef7b6cc0",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
import PageEffectAssemblerBase from "./page-effect-assembler-base";
const gfx = cc.gfx
let vfmtPosUvColorFront = new gfx.VertexFormat([
{ name: gfx.ATTR_POSITION, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
{ name: gfx.ATTR_UV0, type: gfx.ATTR_TYPE_FLOAT32, num: 2 },
{ name: gfx.ATTR_COLOR, type: gfx.ATTR_TYPE_UINT8, num: 4, normalize: true },
{ name: "a_isFront", type: gfx.ATTR_TYPE_FLOAT32, num: 1},
]);
export default class VerletAssembler extends PageEffectAssemblerBase {
init(comp: cc.RenderComponent) {
super.init(comp);
//@ts-ignore
let segmentCount = comp.pointsCount - 1;
this.verticesCount = 4 * segmentCount;
this.indicesCount = 6 * segmentCount;
this.floatsPerVert = 6;
this.initData();
}
getVfmt() {
return vfmtPosUvColorFront;
}
public updateRenderData (comp: any) {
if (comp) {
let pointList: cc.Vec2[] = comp.getPointList()
let pointNum: number = pointList.length
if (pointNum < 2) {
return
}
let node = comp.node
let height = node.height
let width = node.width
let posX = - width * node.anchorX
let posY = - height * node.anchorY
let gapU = 1 / (pointNum - 1)
let lastU = 0
let nextU = 0
let floatsPerVert = this.floatsPerVert;
let verts = this.renderData.vDatas[0];
// 写verts时的下标
let dstOffset = 0;
for (let i = 1; i < pointNum; i++) {
let lastPoint = pointList[i - 1]
let nextPoint = pointList[i]
nextU = lastU + gapU
// 顶点和质点一一对应
// 顶点数据写入verts
dstOffset = floatsPerVert * (i-1) * 4;
verts[dstOffset] = posX + lastPoint.x;
verts[dstOffset + 1] = posY + lastPoint.y;
verts[dstOffset + 2] = lastU;
verts[dstOffset + 3] = 1;
dstOffset += floatsPerVert;
verts[dstOffset] = posX + nextPoint.x;
verts[dstOffset + 1] = posY + nextPoint.y;
verts[dstOffset + 2] = nextU;
verts[dstOffset + 3] = 1;
dstOffset += floatsPerVert;
verts[dstOffset] = posX + lastPoint.x;
verts[dstOffset + 1] = posY + height + lastPoint.y;
verts[dstOffset + 2] = lastU;
verts[dstOffset + 3] = 0;
dstOffset += floatsPerVert;
verts[dstOffset] = posX + nextPoint.x;
verts[dstOffset + 1] = posY + height + nextPoint.y;
verts[dstOffset + 2] = nextU;
verts[dstOffset + 3] = 0;
lastU = nextU
}
this.updateColor(comp, null);
this.updateIsFront(comp, 5);
}
}
}
\ No newline at end of file
{
"ver": "1.0.8",
"uuid": "b2306dc3-a1a0-4fe9-8300-47f0c5da21b9",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
import VerletAssembler from "./verlet-assembler";
const {ccclass, property} = cc._decorator;
class PagePoint {
public oldPos:cc.Vec2
public newPos:cc.Vec2
constructor(x:number, y:number){
this.oldPos = this.newPos = cc.v2(x, y)
}
}
@ccclass
export default class VerletRender extends cc.RenderComponent {
@property({type: [cc.Texture2D], displayName:"纹理"})
public textureList: cc.Texture2D[] = []
@property({displayName:"每条边上的顶点数量"})
public pointsCount:number = 30
@property({displayName:"纠正次数"})
public constraintTimes: number = 100
@property({displayName:"速度衰减系数"})
public damping: number = 0.1
@property({displayName:"重力"})
public gravity: number = 0
protected _initedMaterial:boolean = false
private _pointList: PagePoint[] = []
private _angle: number = 0
onEnable () {
super.onEnable();
this.initPointList();
this.draw();
}
public _resetAssembler() {
let assembler = this._assembler = new VerletAssembler()
assembler.init(this)
}
protected _updateMaterial() {
let material = this.getMaterial(0)
if (material) {
material.define('CC_USE_MODEL', 1);
if (this.textureList.length === 2) {
material.setProperty('texture0', this.textureList[0]);
material.setProperty('texture1', this.textureList[1]);
}
}
}
protected updateMaterial () {
if (this.textureList.length === 2) {
this._updateMaterial()
this._initedMaterial = true
return
}
}
public updateAngle(angle: number) {
this._angle = angle
}
public getPointList() {
let pointList: cc.Vec2[] = []
for(let point of this._pointList) {
pointList.push(new cc.Vec2(point.newPos.x, point.newPos.y))
}
return pointList
}
// 初始化质点
public initPointList() {
for(let i = 0; i < this.pointsCount; ++i) {
let posX = i / (this.pointsCount - 1) * this.node.width
this._pointList.push(new PagePoint(posX, 0))
}
}
public update() {
this.simulate()
this.applyConstraint()
this.draw()
}
// 使用verlet积分更新位置
public simulate() {
let gravity = cc.v2(0, this.gravity)
for (let i = this.pointsCount - 1; i >= 1; i--) {
let point = this._pointList[i]
// 速度等于当前位置与上一个位置的差乘上衰减系数
let velocity: cc.Vec2 = point.newPos.sub(point.oldPos).mul(this.damping)
// 模拟一个水平放置的绳子,当y小于等于0时,将不再受重力影响
if(point.newPos.y <= 0) {
gravity.y = Math.max(0, gravity.y)
}
point.oldPos = point.newPos
point.newPos = point.newPos.add(velocity)
point.newPos = point.newPos.add(gravity)
}
}
private _updateEndPos(endPos: cc.Vec2) {
let tailPoint = this._pointList[this.pointsCount - 1]
tailPoint.newPos = new cc.Vec2(endPos.x, endPos.y)
}
private _getEndPos(): cc.Vec2 {
let endPos = new cc.Vec2(0, 0)
let width = this.node.width
let rad = this._angle * Math.PI / 180
// 与贝塞尔曲线使用相同的运动轨迹
let per = rad * 2 / Math.PI
if(this._angle <= 90) {
let endPosX = width * (1 - Math.pow(per, 3))
let endPosY = width * 1 / 4 * (1 - Math.pow(1 - per, 4))
endPos = new cc.Vec2(endPosX, endPosY)
} else {
per = per - 1
let endPosX = - width * (1 - Math.pow(1 - per, 3))
let endPosY = width * 1 / 4 * (1 - Math.pow(per, 4))
endPos = new cc.Vec2(endPosX, endPosY)
}
return endPos
}
// 约束纠正
public applyConstraint() {
// 两个质点之间的固定距离
let normalDistance = this.node.width / (this.pointsCount - 1)
let endPos = this._getEndPos()
for (let t = 0; t < this.constraintTimes; t++) {
this._updateEndPos(endPos)
//由最后一个质点开始依次纠正
for (let i = this.pointsCount - 1; i >= 1; i--) {
let firstPoint = this._pointList[i - 1]
let secondPoint = this._pointList[i]
let delatPos = secondPoint.newPos.sub(firstPoint.newPos)
let distance = delatPos.mag()
let fixDirection :cc.Vec2 = null
if (distance < normalDistance) {
fixDirection = delatPos.normalize().negate()
} else if (distance > normalDistance) {
fixDirection = delatPos.normalize()
} else {
continue
}
let fixLen = Math.abs(distance - normalDistance)
if (i == 1) {
// 由于第一个质点是固定的,所以只对第二个质点做纠正
let fixVector = fixDirection.mul(fixLen)
secondPoint.newPos.subSelf(fixVector)
} else {
// 将两个质点之间的距离纠正为固定长度
let fixHalfVector = fixDirection.mul(fixLen * 0.5)
firstPoint.newPos.addSelf(fixHalfVector)
secondPoint.newPos.subSelf(fixHalfVector)
}
}
}
}
public draw() {
if (!this._initedMaterial) {
this.updateMaterial()
}
this.setVertsDirty()
}
}
\ No newline at end of file
{
"ver": "1.0.8",
"uuid": "2705a6da-425b-4b03-9c3e-520473fb3ab3",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}
\ No newline at end of file
{
"ver": "1.1.2",
"uuid": "5b9ab1e7-4aca-4a8b-b04a-ad06b39da6ef",
"isBundle": false,
"bundleName": "",
"priority": 1,
"compressionType": {},
"optimizeHotUpdate": {},
"inlineSpriteFrames": {},
"isRemoteBundle": {},
"subMetas": {}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "cabced38-3e05-4e9f-82db-731b9e7a868a",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 1279,
"height": 913,
"platformSettings": {},
"subMetas": {
"img_bg": {
"ver": "1.0.4",
"uuid": "ff5c09a2-40c2-44f6-9da2-764abe211e85",
"rawTextureUuid": "cabced38-3e05-4e9f-82db-731b9e7a868a",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1279,
"height": 913,
"rawWidth": 1279,
"rawHeight": 913,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "9ba4e9ae-2ae4-4918-9160-aab34f980bfb",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 1384,
"height": 1948,
"platformSettings": {},
"subMetas": {
"dg_test_01": {
"ver": "1.0.4",
"uuid": "f0addbcd-babd-4d66-9d14-2c819ec70671",
"rawTextureUuid": "9ba4e9ae-2ae4-4918-9160-aab34f980bfb",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1384,
"height": 1948,
"rawWidth": 1384,
"rawHeight": 1948,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "ab70a7c9-7166-442f-ab53-79df83f5b690",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 1381,
"height": 1947,
"platformSettings": {},
"subMetas": {
"dg_test_02": {
"ver": "1.0.4",
"uuid": "9c4f0195-cd8d-4f17-9418-8cd1c8b9b574",
"rawTextureUuid": "ab70a7c9-7166-442f-ab53-79df83f5b690",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1381,
"height": 1947,
"rawWidth": 1381,
"rawHeight": 1947,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "59d246d5-ccae-4cd9-a40f-64d8c3e08f64",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 1377,
"height": 1950,
"platformSettings": {},
"subMetas": {
"dg_test_03": {
"ver": "1.0.4",
"uuid": "8e634a8e-2a54-4a3e-9a8f-acc8815dc85e",
"rawTextureUuid": "59d246d5-ccae-4cd9-a40f-64d8c3e08f64",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1377,
"height": 1950,
"rawWidth": 1377,
"rawHeight": 1950,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "bd25817f-6943-4dec-9de8-4848a275ae7b",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 1422,
"height": 800,
"platformSettings": {},
"subMetas": {
"image-1": {
"ver": "1.0.4",
"uuid": "67e55efb-27b7-4363-ac03-ed8169493b57",
"rawTextureUuid": "bd25817f-6943-4dec-9de8-4848a275ae7b",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1422,
"height": 800,
"rawWidth": 1422,
"rawHeight": 800,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "86a4494f-a036-4bb6-8e26-94d70dee481e",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 889,
"height": 500,
"platformSettings": {},
"subMetas": {
"image-2": {
"ver": "1.0.4",
"uuid": "7bdee920-7f7e-46db-b473-200e9616b4a6",
"rawTextureUuid": "86a4494f-a036-4bb6-8e26-94d70dee481e",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 889,
"height": 500,
"rawWidth": 889,
"rawHeight": 500,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "ea240721-8894-4bf5-a737-56d568fa0e5d",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 1422,
"height": 800,
"platformSettings": {},
"subMetas": {
"image-3": {
"ver": "1.0.4",
"uuid": "0ee4eaa8-b9ec-42c3-80c2-6e7934615e74",
"rawTextureUuid": "ea240721-8894-4bf5-a737-56d568fa0e5d",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 1422,
"height": 800,
"rawWidth": 1422,
"rawHeight": 800,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
{
"ver": "2.3.5",
"uuid": "c4cac2bf-83ff-4e65-aad5-b32055a2ba90",
"type": "sprite",
"wrapMode": "clamp",
"filterMode": "bilinear",
"premultiplyAlpha": false,
"genMipmaps": false,
"packable": true,
"width": 500,
"height": 281,
"platformSettings": {},
"subMetas": {
"image-4": {
"ver": "1.0.4",
"uuid": "0173fddc-c9da-4940-94c2-62cd4cac53b1",
"rawTextureUuid": "c4cac2bf-83ff-4e65-aad5-b32055a2ba90",
"trimType": "auto",
"trimThreshold": 1,
"rotated": false,
"offsetX": 0,
"offsetY": 0,
"trimX": 0,
"trimY": 0,
"width": 500,
"height": 281,
"rawWidth": 500,
"rawHeight": 281,
"borderTop": 0,
"borderBottom": 0,
"borderLeft": 0,
"borderRight": 0,
"subMetas": {}
}
}
}
\ No newline at end of file
<div class="d-flex" (drop)="handle_drop($event)" (dragover)="handle_dragover($event)">
<div class="d-flex">
<div class="p-btn-record d-flex">
<div class="btn-clear" style="cursor: pointer" (click)="onBtnClearAudio()" *ngIf="withRmBtn && (audioUrl || audioBlob)">
<fa-icon icon="times"></fa-icon>
......
......@@ -276,39 +276,6 @@ export class AudioRecorderComponent implements OnInit, OnChanges, OnDestroy {
this.progress = Math.floor(p * 100);
}
linkInputed (url, name) {
url = url.substring(0, url.lastIndexOf(".")) + "_l.mp3";
this.audioUrl = url;
this.audioUploaded.emit({url});
this.audioName.emit(name);
}
handle_dragover(e) {
e.preventDefault();
}
handle_drop(e) {
const dt = e.dataTransfer.getData("text/plain");
console.log("handle_drop===", dt);
if (!dt) {
return;
}
try {
const {url, name} = JSON.parse(dt);
if (url.indexOf("teach")<0 || url.indexOf("cdn")<0) {
return;
}
const white = [".mp3"];
if (!white.includes(url.substr(url.lastIndexOf(".")))) {
return;
}
this.linkInputed(url, name);
} catch (error) {
console.warn("handle_drop拖拽在线音频传递参数不合法,应该是{url:'', name:''}");
}
}
}
enum Type {
......
<div class="position-relative" (drop)="handle_drop($event)" (dragover)="handle_dragover($event)">
<div class="position-relative">
<button nz-button (click)="setAnimaBtnClick()" style=" border-radius: 0.5rem; border: 1px solid #ddd;" (dragover)="handle_dragover($event)">
<button nz-button (click)="setAnimaBtnClick()" style=" border-radius: 0.5rem; border: 1px solid #ddd;">
<i nz-icon nzType="tool" nzTheme="outline"></i>
{{btnName}}
</button>
......
<div class="position-relative" (drop)="handle_drop($event)" (dragover)="handle_dragover($event)">
<div class="position-relative">
<nz-upload class="p-image-uploader" [nzDisabled]="disableUpload"
[nzShowUploadList]="false"
nzAccept = "image/*"
[nzAction]="uploadUrl"
[nzData]="uploadData"
(nzChange)="handleChange($event)"
>
(nzChange)="handleChange($event)">
<!--[nzBeforeUpload]="customUpload">-->
<div class="p-box d-flex align-items-center">
<div class="p-upload-icon" *ngIf="!picUrl && !uploading">
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment