Commit 659aef97 authored by 李维's avatar 李维

optimize code

parent b4f85ada
......@@ -13,19 +13,19 @@ import { FormComponent } from './form/form.component';
import { PlayComponent } from "./play/play.component";
// import { ColorPickerModule } from 'ngx-color-picker';
import { LessonTitleConfigComponent } from './common/lesson-title-config/lesson-title-config.component';
// import { LessonTitleConfigComponent } from './common/lesson-title-config/lesson-title-config.component';
import { AudioRecorderComponent } from './common/audio-recorder/audio-recorder.component';
import { PlayerContentWrapperComponent } from './common/player-content-wrapper/player-content-wrapper.component';
// import { PlayerContentWrapperComponent } from './common/player-content-wrapper/player-content-wrapper.component';
/** 配置 angular i18n **/
import { registerLocaleData } from '@angular/common';
import zh from '@angular/common/locales/zh';
import {UploadImageWithPreviewComponent} from "./common/upload-image-with-preview/upload-image-with-preview.component";
import {BackgroundImagePipe} from "./pipes/background-image.pipe";
import {UploadVideoComponent} from "./common/upload-video/upload-video.component";
// import {UploadVideoComponent} from "./common/upload-video/upload-video.component";
import {ResourcePipe} from "./pipes/resource.pipe";
import {TimePipe} from "./pipes/time.pipe";
import {CustomHotZoneComponent} from "./common/custom-hot-zone/custom-hot-zone.component";
// import {CustomHotZoneComponent} from "./common/custom-hot-zone/custom-hot-zone.component";
registerLocaleData(zh);
@NgModule({
......@@ -33,15 +33,11 @@ registerLocaleData(zh);
AppComponent,
FormComponent,
PlayComponent,
LessonTitleConfigComponent,
AudioRecorderComponent,
UploadImageWithPreviewComponent,
BackgroundImagePipe,
ResourcePipe,
TimePipe,
UploadVideoComponent,
CustomHotZoneComponent,
PlayerContentWrapperComponent
UploadImageWithPreviewComponent,
],
imports: [
FormsModule,
......
import TWEEN from '@tweenjs/tween.js';
class Sprite {
x = 0;
y = 0;
color = '';
radius = 0;
alive = false;
margin = 0;
angle = 0;
ctx;
constructor(ctx) {
this.ctx = ctx;
}
update($event) {
this.draw();
}
draw() {
}
}
export class MySprite extends Sprite {
width = 0;
height = 0;
_anchorX = 0;
_anchorY = 0;
_offX = 0;
_offY = 0;
scaleX = 1;
scaleY = 1;
alpha = 1;
rotation = 0;
visible = true;
children = [this];
img;
_z = 0;
init(imgObj = null, anchorX:number = 0.5, anchorY:number = 0.5) {
if (imgObj) {
this.img = imgObj;
this.width = this.img.width;
this.height = this.img.height;
}
this.anchorX = anchorX;
this.anchorY = anchorY;
}
update($event = null) {
if (this.visible) {
this.draw();
}
}
draw() {
this.ctx.save();
this.drawInit();
this.updateChildren();
this.ctx.restore();
}
drawInit() {
this.ctx.translate(this.x, this.y);
this.ctx.rotate(this.rotation * Math.PI / 180);
this.ctx.scale(this.scaleX, this.scaleY);
this.ctx.globalAlpha = this.alpha;
}
drawSelf() {
if (this.img) {
this.ctx.drawImage(this.img, this._offX, this._offY);
}
}
updateChildren() {
if (this.children.length <= 0) { return; }
for (let i = 0; i < this.children.length; i++) {
if (this.children[i] === this) {
this.drawSelf();
} else {
this.children[i].update();
}
}
}
load(url, anchorX = 0.5, anchorY = 0.5) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
}).then(img => {
this.init(img, anchorX, anchorY);
return img;
});
}
addChild(child, z = 1) {
if (this.children.indexOf(child) === -1) {
this.children.push(child);
child._z = z;
child.parent = this;
}
this.children.sort((a, b) => {
return a._z - b._z;
});
}
removeChild(child) {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
}
}
set anchorX(value) {
this._anchorX = value;
this.refreshAnchorOff();
}
get anchorX() {
return this._anchorX;
}
set anchorY(value) {
this._anchorY = value;
this.refreshAnchorOff();
}
get anchorY() {
return this._anchorY;
}
refreshAnchorOff() {
this._offX = -this.width * this.anchorX;
this._offY = -this.height * this.anchorY;
}
setScaleXY(value) {
this.scaleX = this.scaleY = value;
}
getBoundingBox() {
const x = this.x + this._offX * this.scaleX;
const y = this.y + this._offY * this.scaleY;
const width = this.width * this.scaleX;
const height = this.height * this.scaleY;
return {x, y, width, height};
}
}
export class Item extends MySprite {
baseX;
move(targetY, callBack) {
const self = this;
const tween = new TWEEN.Tween(this)
.to({ y: targetY }, 2500)
.easing(TWEEN.Easing.Quintic.Out)
.onComplete(function() {
self.hide(callBack);
// if (callBack) {
// callBack();
// }
})
.start();
}
show(callBack = null) {
const tween = new TWEEN.Tween(this)
.to({ alpha: 1 }, 800)
// .easing(TWEEN.Easing.Quadratic.Out) // Use an easing function to make the animation smooth.
.onComplete(function() {
if (callBack) {
callBack();
}
})
.start(); // Start the tween immediately.
}
hide(callBack = null) {
const tween = new TWEEN.Tween(this)
.to({ alpha: 0 }, 800)
// .easing(TWEEN.Easing.Quadratic.Out) // Use an easing function to make the animation smooth.
.onComplete(function() {
if (callBack) {
callBack();
}
})
.start(); // Start the tween immediately.
}
shake(id) {
if (!this.baseX) {
this.baseX = this.x;
}
const baseX = this.baseX;
const baseTime = 50;
const sequence = [
{target: {x: baseX + 40 * id}, time: baseTime - 25},
{target: {x: baseX - 20 * id}, time: baseTime},
{target: {x: baseX + 10 * id}, time: baseTime},
{target: {x: baseX - 5 * id}, time: baseTime},
{target: {x: baseX + 2 * id}, time: baseTime},
{target: {x: baseX - 1 * id}, time: baseTime},
{target: {x: baseX}, time: baseTime},
];
const self = this;
function runSequence() {
if (self['shakeTween']) {
self['shakeTween'].stop();
}
const tween = new TWEEN.Tween(self);
if (sequence.length > 0) {
const action = sequence.shift();
tween.to(action['target'], action['time']);
tween.onComplete( () => {
runSequence();
});
tween.start();
self['shakeTween'] = tween;
}
}
runSequence();
}
drop(targetY, callBack = null) {
const self = this;
const time = Math.abs(targetY - this.y) * 2.4;
this.alpha = 1;
const tween = new TWEEN.Tween(this)
.to({ y: targetY }, time)
.easing(TWEEN.Easing.Cubic.In)
.onComplete(function() {
// self.hideItem(callBack);
if (callBack) {
callBack();
}
})
.start();
}
}
export class EndSpr extends MySprite {
show(s) {
this.scaleX = this.scaleY = 0;
this.alpha = 0;
const tween = new TWEEN.Tween(this)
.to({ alpha: 1, scaleX: s, scaleY: s }, 800)
.easing(TWEEN.Easing.Elastic.Out) // Use an easing function to make the animation smooth.
.onComplete(function() {
})
.start(); // Start the tween immediately.
}
}
export class ShapeRect extends MySprite {
fillColor = '#FF0000';
setSize(w, h) {
this.width = w;
this.height = h;
}
drawShape() {
this.ctx.fillStyle = this.fillColor;
this.ctx.fillRect(this._offX, this._offY, this.width, this.height);
}
drawSelf() {
super.drawSelf();
this.drawShape();
}
}
export class HotZoneItem extends MySprite {
lineDashFlag = false;
arrow: MySprite;
label: Label;
text;
arrowTop;
arrowRight;
setSize(w, h) {
this.width = w;
this.height = h;
const rect = new ShapeRect(this.ctx);
rect.x = -w / 2;
rect.y = -h / 2;
rect.setSize(w, h);
rect.fillColor = '#FFFFFF';
rect.alpha = 0.2;
this.addChild(rect);
}
showLabel(text = null) {
if (!this.label) {
this.label = new Label(this.ctx);
this.label.anchorY = 0;
this.label.fontSize = '40px';
this.label.textAlign = 'center';
this.addChild(this.label);
this.refreshLabelScale();
}
if (text) {
this.label.text = text;
} else if (this.text) {
this.label.text = this.text;
}
this.label.visible = true;
}
hideLabel() {
if (!this.label) { return; }
this.label.visible = false;
}
refreshLabelScale() {
if (this.scaleX == this.scaleY) {
this.label.setScaleXY(1);
}
if (this.scaleX > this.scaleY) {
this.label.scaleX = this.scaleY / this.scaleX;
} else {
this.label.scaleY = this.scaleX / this.scaleY;
}
}
showLineDash() {
this.lineDashFlag = true;
if (this.arrow) {
this.arrow.visible = true;
} else {
this.arrow = new MySprite(this.ctx);
this.arrow.load('assets/common/arrow.png', 1, 0);
this.arrow.setScaleXY(0.06);
this.arrowTop = new MySprite(this.ctx);
this.arrowTop.load('assets/common/arrow_top.png', 0.5, 0);
this.arrowTop.setScaleXY(0.06);
this.arrowRight = new MySprite(this.ctx);
this.arrowRight.load('assets/common/arrow_right.png', 1, 0.5);
this.arrowRight.setScaleXY(0.06);
}
this.showLabel();
}
hideLineDash() {
this.lineDashFlag = false;
if (this.arrow) {
this.arrow.visible = false;
}
this.hideLabel();
}
drawArrow() {
if (!this.arrow) { return; }
const rect = this.getBoundingBox();
this.arrow.x = rect.x + rect.width;
this.arrow.y = rect.y;
this.arrow.update();
this.arrowTop.x = rect.x + rect.width / 2;
this.arrowTop.y = rect.y;
this.arrowTop.update();
this.arrowRight.x = rect.x + rect.width;
this.arrowRight.y = rect.y + rect.height / 2;
this.arrowRight.update();
}
drawFrame() {
this.ctx.save();
const rect = this.getBoundingBox();
const w = rect.width;
const h = rect.height;
const x = rect.x + w / 2;
const y = rect.y + h / 2;
this.ctx.setLineDash([5, 5]);
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = '#1bfff7';
this.ctx.beginPath();
this.ctx.moveTo( x - w / 2, y - h / 2);
this.ctx.lineTo(x + w / 2, y - h / 2);
this.ctx.lineTo(x + w / 2, y + h / 2);
this.ctx.lineTo(x - w / 2, y + h / 2);
this.ctx.lineTo(x - w / 2, y - h / 2);
this.ctx.stroke();
this.ctx.restore();
}
draw() {
super.draw();
if (this.lineDashFlag) {
this.drawFrame();
this.drawArrow();
}
}
}
export class HotZoneImageItem extends MySprite {
lineDashFlag = false;
arrow: MySprite;
label: Label;
image: MySprite;
image_url: String;
labelBox: MySprite;
audio_url: String;
card_audio_url: String
scale
text;
arrowTop;
arrowRight;
init(image_url = null, callback?, handleSave?){
this.showImage(image_url, (img)=>{
callback && callback(img.width, img.height)
this.setSize(img.width*this.image.scaleX, img.height*this.image.scaleY)
handleSave && handleSave()
this.lineDashFlag = true;
this.showLineDash()
this.showLabel();
this.drawArrow()
})
}
setSize(w, h) {
this.width = w;
this.height = h;
}
showLabel(text = null) {
if (!this.label) {
this.labelBox = new MySprite(this.ctx);
this.labelBox.load('assets/default/bg_50_50.png').then(()=>{
this.labelBox.x = this.labelBox.width/2;
this.labelBox.y = this.labelBox.height/2;
});
this.label = new Label(this.ctx);
// this.label.anchorY = 0;
this.label.fontSize = '30px';
this.label.textAlign = 'center';
this.label.color = "#FFFFFF"
this.labelBox.addChild(this.label);
this.addChild(this.labelBox);
this.refreshLabelScale();
}
if (text) {
this.label.text = text;
} else if (this.text) {
this.label.text = this.text;
}
this.label.visible = true;
}
hideLabel() {
if (!this.label) { return; }
this.label.visible = false;
}
showImage(image_url = null, callback) {
this.loadImage(image_url).then((img)=>{
if(this.image){
this.removeChild(this.image)
}
this.image = new MySprite(this.ctx);
this.image.init(img)
this.image.x = this.image.width/2
this.image.y = this.image.height/2
this.image.alpha = 0.7;
this.image_url = image_url;
this.addChild(this.image,-1);
callback && callback(this.image)
})
}
loadImage(image_url = null){
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = image_url;
})
}
hideImage() {
if (!this.image) { return; }
this.image.visible = false;
}
refreshLabelScale() {
this.labelBox.scaleX = 75 / (this.labelBox.width*this.image.scaleX)
}
showLineDash() {
if (this.arrow) {
this.arrow.visible = true;
} else {
this.arrow = new MySprite(this.ctx);
this.arrow.load('assets/common/arrow.png', 1, 0);
this.arrow.setScaleXY(0.06);
this.arrowTop = new MySprite(this.ctx);
this.arrowTop.load('assets/common/arrow_top.png', 0.5, 0);
this.arrowTop.setScaleXY(0.06);
this.arrowRight = new MySprite(this.ctx);
this.arrowRight.load('assets/common/arrow_right.png', 1, 0.5);
this.arrowRight.setScaleXY(0.06);
}
}
hideLineDash() {
this.lineDashFlag = false;
if (this.arrow) {
this.arrow.visible = false;
}
this.hideLabel();
}
drawArrow() {
if (!this.arrow) { return; }
const rect = this.getBoundingBox();
this.arrow.x = rect.x + rect.width;
this.arrow.y = rect.y;
this.arrow.update();
this.arrowTop.x = rect.x + rect.width / 2;
this.arrowTop.y = rect.y;
this.arrowTop.update();
this.arrowRight.x = rect.x + rect.width;
this.arrowRight.y = rect.y + rect.height / 2;
this.arrowRight.update();
}
drawFrame() {
this.ctx.save();
const rect = this.getBoundingBox();
const w = rect.width;
const h = rect.height;
const x = rect.x + w / 2;
const y = rect.y + h / 2;
this.ctx.setLineDash([5, 5]);
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = '#1bfff7';
this.ctx.beginPath();
this.ctx.moveTo( x - w / 2, y - h / 2);
this.ctx.lineTo(x + w / 2, y - h / 2);
this.ctx.lineTo(x + w / 2, y + h / 2);
this.ctx.lineTo(x - w / 2, y + h / 2);
this.ctx.lineTo(x - w / 2, y - h / 2);
this.ctx.stroke();
this.ctx.restore();
}
draw() {
super.draw();
if (this.lineDashFlag) {
this.drawFrame();
this.drawArrow();
}
}
}
export class EditorItem extends MySprite {
lineDashFlag = false;
arrow: MySprite;
label:Label;
text;
showLabel(text = null) {
if (!this.label) {
this.label = new Label(this.ctx);
this.label.anchorY = 0;
this.label.fontSize = '50px';
this.label.textAlign = 'center';
this.addChild(this.label);
this.label.setScaleXY(1 / this.scaleX);
}
if (text) {
this.label.text = text;
} else if (this.text) {
this.label.text = this.text;
}
this.label.visible = true;
}
hideLabel() {
if (!this.label) { return; }
this.label.visible = false;
}
showLineDash() {
this.lineDashFlag = true;
if (this.arrow) {
this.arrow.visible = true;
} else {
this.arrow = new MySprite(this.ctx);
this.arrow.load('assets/common/arrow.png', 1, 0);
this.arrow.setScaleXY(0.06);
}
this.showLabel();
}
hideLineDash() {
this.lineDashFlag = false;
if (this.arrow) {
this.arrow.visible = false;
}
this.hideLabel();
}
drawArrow() {
if (!this.arrow) { return; }
const rect = this.getBoundingBox();
this.arrow.x = rect.x + rect.width;
this.arrow.y = rect.y;
this.arrow.update();
}
drawFrame() {
this.ctx.save();
const rect = this.getBoundingBox();
const w = rect.width;
const h = rect.height;
const x = rect.x + w / 2;
const y = rect.y + h / 2;
this.ctx.setLineDash([5, 5]);
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = '#1bfff7';
// this.ctx.fillStyle = '#ffffff';
this.ctx.beginPath();
this.ctx.moveTo( x - w / 2, y - h / 2);
this.ctx.lineTo(x + w / 2, y - h / 2);
this.ctx.lineTo(x + w / 2, y + h / 2);
this.ctx.lineTo(x - w / 2, y + h / 2);
this.ctx.lineTo(x - w / 2, y - h / 2);
// this.ctx.fill();
this.ctx.stroke();
this.ctx.restore();
}
draw() {
super.draw();
if (this.lineDashFlag) {
this.drawFrame();
this.drawArrow();
}
}
}
export class Label extends MySprite {
text:String;
fontSize:String = '40px';
fontName:String = 'Verdana';
textAlign:String = 'left';
constructor(ctx) {
super(ctx);
this.init();
}
drawText() {
if (!this.text) { return; }
this.ctx.font = `${this.fontSize} ${this.fontName}`;
this.ctx.textAlign = this.textAlign;
this.ctx.textBaseline = 'middle';
this.ctx.fontWeight = 900;
this.ctx.lineWidth = 5;
this.ctx.strokeStyle = '#ffffff';
this.ctx.strokeText(this.text, 0, 0);
this.ctx.fillStyle = '#000000';
this.ctx.fillText(this.text, 0, 0);
}
drawSelf() {
super.drawSelf();
this.drawText();
}
}
export function getPosByAngle(angle, len) {
const radian = angle * Math.PI / 180;
const x = Math.sin(radian) * len;
const y = Math.cos(radian) * len;
return {x, y};
}
export function getAngleByPos(px, py, mx, my) {
const x = Math.abs(px - mx);
const y = Math.abs(py - my);
const z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
const cos = y / z;
const radina = Math.acos(cos); // 用反三角函数求弧度
let angle = Math.floor(180 / (Math.PI / radina) * 100) / 100; // 将弧度转换成角度
if(mx > px && my > py) {// 鼠标在第四象限
angle = 180 - angle;
}
if(mx === px && my > py) {// 鼠标在y轴负方向上
angle = 180;
}
if(mx > px && my === py) {// 鼠标在x轴正方向上
angle = 90;
}
if(mx < px && my > py) {// 鼠标在第三象限
angle = 180 + angle;
}
if(mx < px && my === py) {// 鼠标在x轴负方向
angle = 270;
}
if(mx < px && my < py) {// 鼠标在第二象限
angle = 360 - angle;
}
return angle;
}
<div class="p-image-children-editor">
<div style="float: left; width: 25%;border-right: 2px solid #ddd; border-bottom: 2px solid #ddd;">
<h5 style="margin-left: 2.5%;">预览:</h5>
<div id="canvas-container" style="margin:5px;">
<div class="preview-box" #wrap style="margin-bottom: 20px">
<canvas id="canvas" #canvas></canvas>
</div>
</div>
<div style="text-align: center">
<button nz-button nzType="primary" [nzSize]="'small'" nzShape="round" (click)="saveClick()" [disabled]="!hotZoneChanged" >
<i nz-icon type="save"></i>
Save
</button>
</div>
<h5 style="border-top: 2px solid #ddd;">背景图:</h5>
<div style="width: 200px;margin: auto;">
<div class="bg-box">
<app-upload-image-with-preview
[picUrl]="bgItem.url"
(imageUploaded)="onBackgroundUploadSuccess($event)"
>
</app-upload-image-with-preview>
</div>
</div>
</div>
<div style="float: left; width: 75%">
<div nzAlign="middle">
<div class="img-box clearfix" *ngFor="let it of hotZoneArr; let i = index" style="border-bottom: 1px solid #DDD; padding-bottom: 10px;" >
<div style="float: left; height: 115px; position: relative; width: 250px; ">
<h3> 第-{{ i + 1 }}-题 </h3>
<div style="position: absolute; bottom: 0;">
<h5> 题目音频 </h5>
<app-audio-recorder [audioUrl]="it.audio_url ? it.audio_url : null" (audioUploaded)="onAudioUploadSuccess($event, it, false)" ></app-audio-recorder>
</div>
</div>
<div style="float: left; height: 115px; position: relative; width: 250px; ">
<div style="position: absolute; bottom: 0;">
<h5> 卡片音频 </h5>
<app-audio-recorder [audioUrl]="it.card_audio_url ? it.card_audio_url : null" (audioUploaded)="onAudioUploadSuccess($event, it, true)" ></app-audio-recorder>
</div>
</div>
<div style="float: left;">
<div style="width:200px">
<app-upload-image-with-preview [picUrl]="it.image_url" (imageUploaded)="onImgUploadSuccessByImg($event, it)" ></app-upload-image-with-preview>
</div>
</div>
<div style="float: left; display: flex; flex-direction: column; padding-left: 10px;">
<button style="flex:1; margin-bottom: 5px;" nz-button nzType="dashed" (click)="handleMoveItemUp($event, i)" [disabled]="i==0">
<i nz-icon nzType="up" nzTheme="outline"></i>
上移
</button>
<button style="flex:1; margin-bottom: 5px;" nz-button nzType="dashed" (click)="handleMoveItemDown($event, i)" [disabled]="i==hotZoneArr.length-1">
<i nz-icon nzType="down" nzTheme="outline"></i>
下移
</button>
<button style="flex:1; margin-bottom: 5px;" nz-button nzType="danger" (click)="deleteItem($event, i)">
<i nz-icon nzType="delete" nzTheme="outline"></i>
删除
</button>
</div>
</div>
<div nz-col nzSpan="5" nzOffset="1">
<div class="bg-box">
<button
nz-button
nzType="dashed"
(click)="addBtnClick()"
class="add-btn"
>
<i nz-icon nzType="plus-circle" nzTheme="outline"></i>
<!--Add Image-->
Add hot zone
</button>
</div>
</div>
</div>
</div>
<nz-divider></nz-divider>
<div class="save-box">
</div>
</div>
.p-image-children-editor {
width: 100%;
height: 100%;
border-radius: 0.5rem;
min-width: 1200px;
border: 2px solid #ddd;
.preview-box {
margin: auto;
width: 100%;
// height: 35vw;
border: 2px dashed #ddd;
border-radius: 0.5rem;
background-color: #fafafa;
text-align: center;
color: #aaa;
.preview-img {
height: 100%;
width: auto;
}
}
.bg-box {
margin-bottom: 1rem;
}
.clearfix:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.img-box {
margin: 1rem;
}
.img-box-upload {
width: 120px;
height: 80px;
}
.add-btn {
margin-top: 1rem;
width: 200px;
height: 90px;
display: flex;
align-items: center;
justify-content: center;
}
}
.save-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
.save-btn {
width: 160px;
height: 40px;
//margin-top: 20px;
margin-bottom: 20px;
display: flex;
align-items: center;
justify-content: center;
}
}
h5 {
margin-top: 1rem;
}
//@import '../../../style/common_mixin';
//
//.p-image-uploader {
// position: relative;
// display: block;
// width: 100%;
// height: 0;
// padding-bottom: 56.25%;
// .p-box {
// position: absolute;
// left: 0;
// top: 0;
// right: 0;
// bottom: 0;
// border: 2px dashed #ddd;
// border-radius: 0.5rem;
// background-color: #fafafa;
// text-align: center;
// color: #aaa;
// .p-upload-icon {
// text-align: center;
// margin: auto;
// .anticon-upload {
// color: #888;
// font-size: 5rem;
// }
// .p-progress-bar {
// position: relative;
// width: 20rem;
// height: 1.5rem;
// border: 1px solid #ccc;
// border-radius: 1rem;
// .p-progress-bg {
// background-color: #1890ff;
// border-radius: 1rem;
// height: 100%;
// }
// .p-progress-value {
// position: absolute;
// top: 0;
// left: 0;
// width: 100%;
// height: 100%;
// text-shadow: 0 0 4px #000;
// color: white;
// text-align: center;
// font-size: 0.9rem;
// line-height: 1.5rem;
// }
// }
// }
// .p-preview {
// width: 100%;
// height: 100%;
// //background-image: url("https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png");
// @include k-img-bg();
// }
// }
//}
//
//.p-btn-delete {
// position: absolute;
// right: -0.5rem;
// top: -0.5rem;
// width: 2rem;
// height: 2rem;
// border: 0.2rem solid white;
// border-radius: 50%;
// font-size: 1.2rem;
// background-color: #fb781a;
// color: white;
// text-align: center;
//}
//
//.p-upload-progress-bg {
// position: absolute;
// width: 100%;
// height: 100%;
// display: flex;
// align-items: center;
// justify-content: center;
// & .i-text {
// position: absolute;
// text-align: center;
// color: white;
// text-shadow: 0 0 2px rgba(0, 0, 0, .85);
// }
// & .i-bg {
// position: absolute;
// left: 0;
// top: 0;
// height: 100%;
// background-color: #27b43f;
// border-radius: 0.5rem;
// }
//}
//
//
//:host ::ng-deep .ant-upload {
// display: block;
//}
import {Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnDestroy, OnInit, Output, ViewChild} from '@angular/core';
import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
import {EditorItem, HotZoneImageItem, Label, MySprite} from './Unit';
import TWEEN from '@tweenjs/tween.js';
@Component({
selector: 'app-custom-hot-zone',
templateUrl: './custom-hot-zone.component.html',
styleUrls: ['./custom-hot-zone.component.scss']
})
export class CustomHotZoneComponent implements OnInit, OnDestroy, OnChanges {
_bgItem = null;
@Input()
set bgItem(v) {
this._bgItem = v;
this.init();
}
get bgItem() {
return this._bgItem;
}
@Input()
imgItemArr = null;
@Input()
hotZoneItemArr = null;
@Input()
hotZoneArr = null;
@Output()
save = new EventEmitter();
@ViewChild('canvas') canvas: ElementRef;
@ViewChild('wrap') wrap: ElementRef;
@HostListener('window:resize', ['$event'])
onResize(event) {
this.g_winResizeEventStream.next();
}
g_winResizeEventStream = new Subject();
canvasWidth = 1280;
canvasHeight = 720;
canvasBaseW = 1280;
canvasBaseH = 720;
mapScale = 1;
ctx;
mx;
my; // 点击坐标
// 资源
// rawImages = new Map(res);
// 声音
bgAudio = new Audio();
images = new Map();
animationId: any;
// winResizeEventStream = new Subject();
canvasLeft;
canvasTop;
renderArr;
imgArr = [];
oldPos;
curItem;
bg: MySprite;
changeSizeFlag = false;
changeTopSizeFlag = false;
changeRightSizeFlag = false;
hotZoneChanged = false;
scale = 1;
constructor(private el:ElementRef) {
}
ngOnInit() {
this.initListener();
this.update();
}
ngOnDestroy() {
window.cancelAnimationFrame(this.animationId);
}
ngOnChanges() {
}
onBackgroundUploadSuccess(e) {
this.bgItem.url = e.url;
this.refreshBackground(()=>{
this.autoSave()
});
}
refreshBackground(callBack = null) {
if (!this.bg) {
this.bg = new MySprite(this.ctx);
this.renderArr.push(this.bg);
}
const bg = this.bg;
if (this.bgItem.url) {
bg.load(this.bgItem.url).then(() => {
const rate1 = this.canvasWidth / bg.width;
const rate2 = this.canvasHeight / bg.height;
const rate = Math.min(rate1, rate2);
bg.setScaleXY(rate);
bg.x = this.canvasWidth / 2;
bg.y = this.canvasHeight / 2;
if (callBack) {
callBack();
}
}).catch((error)=>{
console.log(error)
})
}
}
addBtnClick() {
const item = this.getHotZoneItem();
this.hotZoneArr.push(item);
this.refreshHotZoneId();
this.autoSave()
}
handleMoveItemUp(event,index){
if(index!=0){
this.hotZoneArr[index] = this.hotZoneArr.splice(index-1, 1, this.hotZoneArr[index])[0];
}else{
this.hotZoneArr.push(this.hotZoneArr.shift());
}
this.autoSave()
}
handleMoveItemDown(event,index){
if(index!=this.hotZoneArr.length-1){
this.hotZoneArr[index] = this.hotZoneArr.splice(index+1, 1, this.hotZoneArr[index])[0];
}else{
this.hotZoneArr.unshift( this.hotZoneArr.splice(index,1)[0]);
}
this.autoSave()
}
onImgUploadSuccessByImg(e, item) {
item.init(e.url,(w, h)=>{
item.setScaleXY(Math.min(100 / w, 150 / h))
})
item.pic_url = e.url;
item.image_url = e.url;
this.refreshImage(item);
this.autoSave()
}
onAudioUploadSuccess(e, item, isCardAudio) {
if(isCardAudio){
item.card_audio_url = e.url;
}else{
item.audio_url = e.url;
}
this.autoSave()
}
refreshImage(img) {
this.hideAllLineDash();
img.picItem = this.getPicItem(img);
this.refreshImageId();
}
refreshHotZoneId() {
for (let i = 0; i < this.hotZoneArr.length; i++) {
this.hotZoneArr[i].index = i;
if (this.hotZoneArr[i]) {
this.hotZoneArr[i].text = (i + 1);
}
}
}
refreshImageId() {
for (let i = 0; i < this.imgArr.length; i++) {
this.imgArr[i].id = i;
if (this.imgArr[i].picItem) {
this.imgArr[i].picItem.text = 'Image-' + (i + 1);
}
}
}
getHotZoneItem( saveData = null, newRect?) {
const itemW = 200;
const itemH = 200;
const item = new HotZoneImageItem(this.ctx);
if(saveData){
item.init(saveData.media.image_url, (w, h)=>{
const saveRect = saveData.rect;
item.scaleX = (saveData.rect.width/saveData.scale) / w;
item.scaleY = (saveData.rect.height/saveData.scale) / h;
item.x = saveRect.x / saveData.scale + newRect.x
item.y = saveRect.y / saveData.scale + newRect.y
});
item.image_url = saveData.media.image_url
item.audio_url = saveData.media.audio_url
item.card_audio_url = saveData.media.card_audio_url
}else{
item.init("assets/default/bg_200_200.png" , ()=>{
item.image_url = "assets/default/bg_200_200.png"
item.x = this.canvasWidth / 2 - 100;
item.y = this.canvasHeight / 2 - 100;
},()=>this.autoSave());
}
item.anchorX = 0.5;
item.anchorY = 0.5;
return item;
}
getPicItem(img, saveData = null) {
const item = new EditorItem(this.ctx);
item.load(img.pic_url).then( img => {
let maxW, maxH;
if (this.bg) {
maxW = this.bg.width * this.bg.scaleX;
maxH = this.bg.height * this.bg.scaleY;
} else {
maxW = this.canvasWidth;
maxH = this.canvasHeight;
}
let scaleX = maxW / 3 / item.width;
let scaleY = maxH / 3 / item.height;
if (item.height * scaleX < this.canvasHeight) {
item.setScaleXY(scaleX);
} else {
item.setScaleXY(scaleY);
}
item.x = this.canvasWidth / 2;
item.y = this.canvasHeight / 2;
if (saveData) {
const saveRect = saveData.rect;
item.setScaleXY(saveRect.width / item.width);
item.x = saveRect.x + saveRect.width / 2 ;
item.y = saveRect.y + saveRect.height / 2;
} else {
item.showLineDash();
}
this.autoSave()
})
return item;
}
onAudioUploadSuccessByImg(e, img) {
img.audio_url = e.url;
this.autoSave()
}
deleteItem(e, i) {
this.hotZoneArr.splice(i, 1);
this.refreshHotZoneId();
this.autoSave()
}
init() {
this.initData();
this.initCtx();
this.initItem();
}
initItem() {
if (!this.bgItem) {
this.bgItem = {};
} else {
this.refreshBackground(() => {
if (!this.hotZoneItemArr) {
this.hotZoneItemArr = [];
} else {
this.initHotZoneArr();
}
});
}
}
initHotZoneArr() {
let curBgRect;
if (this.bg) {
curBgRect = this.bg.getBoundingBox();
} else {
curBgRect = {x: 0, y: 0, width: this.canvasWidth, height: this.canvasHeight};
}
let oldBgRect = this.bgItem.rect;
if (!oldBgRect) {
oldBgRect = curBgRect;
}
const rate = curBgRect.width / oldBgRect.width;
this.hotZoneArr = [];
const arr = this.hotZoneItemArr.concat();
for (let i = 0; i < arr.length; i++) {
const data = JSON.parse(JSON.stringify(arr[i]));
data.rect.x *= rate;
data.rect.y *= rate;
data.rect.width *= rate;
data.rect.height *= rate;
// data.rect.x += curBgRect.x;
// data.rect.y += curBgRect.y;
const item = this.getHotZoneItem(data, {x:curBgRect.x,y:curBgRect.y});
// c//onsole.log("初始化存储数据", data)
this.hotZoneArr.push(item);
}
this.refreshHotZoneId();
}
initImgArr() {
let curBgRect;
if (this.bg) {
curBgRect = this.bg.getBoundingBox();
} else {
curBgRect = {x: 0, y: 0, width: this.canvasWidth, height: this.canvasHeight};
}
let oldBgRect = this.bgItem.rect;
if (!oldBgRect) {
oldBgRect = curBgRect;
}
const rate = curBgRect.width / oldBgRect.width;
this.imgArr = [];
const arr = this.imgItemArr.concat();
for (let i = 0; i < arr.length; i++) {
const data = JSON.parse(JSON.stringify(arr[i]));
const img = {pic_url: data.pic_url};
data.rect.x *= rate;
data.rect.y *= rate;
data.rect.width *= rate;
data.rect.height *= rate;
data.rect.x += curBgRect.x;
data.rect.y += curBgRect.y;
img['picItem'] = this.getPicItem(img, data);
img['audio_url'] = arr[i].audio_url;
this.imgArr.push(img);
}
this.refreshImageId();
}
initData() {
this.canvasWidth = this.wrap.nativeElement.clientWidth;
this.canvasHeight = (this.wrap.nativeElement.clientWidth/2)*3;
this.scale = 200/ this.wrap.nativeElement.clientWidth
this.mapScale = this.canvasWidth / this.canvasBaseW;
this.renderArr = [];
this.bg = null;
this.imgArr = [];
this.hotZoneArr = [];
}
initCtx() {
this.ctx = this.canvas.nativeElement.getContext('2d');
this.canvas.nativeElement.width = this.canvasWidth;
this.canvas.nativeElement.height = this.canvasHeight;
}
mapDown(event) {
this.oldPos = {x: this.mx, y: this.my};
const arr = this.hotZoneArr;
for (let i = arr.length - 1; i >= 0 ; i--) {
const item = arr[i];
if (item) {
if (this.checkClickTarget(item)) {
if (item.lineDashFlag && this.checkClickTarget(item.arrow)) {
this.changeItemSize(item);
} else if (item.lineDashFlag && this.checkClickTarget(item.arrowTop)) {
this.changeItemTopSize(item);
} else if (item.lineDashFlag && this.checkClickTarget(item.arrowRight)) {
this.changeItemRightSize(item);
} else {
this.changeCurItem(item);
}
this.hotZoneChanged = true;
return;
}
}
}
}
mapMove(event) {
if (!this.curItem) { return; }
if (this.changeSizeFlag) {
this.changeSize();
} else if (this.changeTopSizeFlag) {
this.changeTopSize();
} else if (this.changeRightSizeFlag) {
this.changeRightSize();
} else {
let addX = this.mx - this.oldPos.x;
let addY = this.my - this.oldPos.y;
this.curItem.x += addX;
this.curItem.y += addY;
}
this.oldPos = {x: this.mx, y: this.my};
}
mapUp(event) {
this.curItem = null;
this.changeSizeFlag = false;
this.changeTopSizeFlag = false;
this.changeRightSizeFlag = false;
}
changeSize() {
const rect = this.curItem.getBoundingBox();
let distance = this.my - rect.y;
let lenW = this.mx - rect.x;
let lenH = rect.height - distance
let minLen = 20;
let sx,sy;
if (lenW < minLen) {
lenW = minLen;
}
sx = lenW / this.curItem.width;
if (lenH < minLen) {
lenH = minLen;
}
sy = lenH / this.curItem.height;
this.curItem.y = this.curItem.y + distance
this.curItem.scaleX = sx;
this.curItem.scaleY = sy;
}
changeTopSize() {
const rect = this.curItem.getBoundingBox();
let lenH = rect.y - this.my + rect.height;
this.curItem.y = this.my
let minLen = 20;
let s;
if (lenH < minLen) {
lenH = minLen;
}
s = lenH / this.curItem.height;
this.curItem.setScaleXY(s);
}
changeRightSize() {
const rect = this.curItem.getBoundingBox();
let lenW = this.mx - rect.x;
let minLen = 20;
let s;
if (lenW < minLen) {
lenW = minLen;
}
s = lenW / this.curItem.width;
this.curItem.setScaleXY(s);
}
changeItemSize(item) {
this.curItem = item;
this.changeSizeFlag = true;
}
changeItemTopSize(item) {
this.curItem = item;
this.changeTopSizeFlag = true;
}
changeItemRightSize(item) {
this.curItem = item;
this.changeRightSizeFlag = true;
}
changeCurItem(item) {
this.hideAllLineDash();
this.curItem = item;
this.curItem.showLineDash();
}
hideAllLineDash() {
for (let i = 0; i < this.imgArr.length; i++) {
if (this.imgArr[i].picItem) {
this.imgArr[i].picItem.hideLineDash();
}
}
}
update() {
this.animationId = window.requestAnimationFrame(this.update.bind(this));
// 清除画布内容
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
for (let i = 0; i < this.renderArr.length; i++) {
this.renderArr[i].update(this);
}
this.updateArr(this.hotZoneArr);
TWEEN.update();
}
updateArr(arr) {
if (arr) {
for (let i = 0; i < arr.length; i++) {
arr[i].update();
}
}
}
renderAfterResize() {
let offsetWidth = this.el.nativeElement.querySelector('#canvas-container').offsetWidth
this.el.nativeElement.querySelector('#canvas-container').style.height = '' + offsetWidth/2*3 + 'px'
this.canvasWidth = this.wrap.nativeElement.clientWidth;
this.canvasHeight = this.wrap.nativeElement.clientHeight;
this.init();
}
initListener() {
this.g_winResizeEventStream.pipe(debounceTime(500)).subscribe(data => {
this.renderAfterResize();
});
if (this.IsPC()) {
this.canvas.nativeElement.addEventListener('mousedown', (event) => {
setMxMyByMouse(event);
this.mapDown(event);
});
this.canvas.nativeElement.addEventListener('mousemove', (event) => {
setMxMyByMouse(event);
this.mapMove(event);
});
this.canvas.nativeElement.addEventListener('mouseup', (event) => {
setMxMyByMouse(event);
this.mapUp(event);
});
const setMxMyByMouse = (event) => {
this.mx = event.offsetX;
this.my = event.offsetY;
};
} else {
this.canvas.nativeElement.addEventListener('touchstart', (event) => {
setMxMyByTouch(event);
this.mapDown(event);
});
this.canvas.nativeElement.addEventListener('touchmove', (event) => {
setMxMyByTouch(event);
this.mapMove(event);
});
this.canvas.nativeElement.addEventListener('touchend', (event) => {
setMxMyByTouch(event);
this.mapUp(event);
});
this.canvas.nativeElement.addEventListener('touchcancel', (event) => {
setMxMyByTouch(event);
this.mapUp(event);
});
const setMxMyByTouch = (event) => {
if (event.touches.length <= 0) {
return;
}
if (this.canvasLeft == null) {
setParentOffset();
}
this.mx = event.touches[0].pageX - this.canvasLeft;
this.my = event.touches[0].pageY - this.canvasTop;
};
const setParentOffset = () => {
const rect = this.canvas.nativeElement.getBoundingClientRect();
this.canvasLeft = rect.left;
this.canvasTop = rect.top;
};
}
}
IsPC() {
if (window['ELECTRON']) {
return false; // 封装客户端标记
}
if (document.body.ontouchstart !== undefined) {
return false;
} else {
return true;
}
}
checkClickTarget(target) {
const rect = target.getBoundingBox();
if (this.checkPointInRect(this.mx, this.my, rect)) {
return true;
}
return false;
}
checkPointInRect(x, y, rect) {
if (x >= rect.x && x <= rect.x + rect.width) {
if (y >= rect.y && y <= rect.y + rect.height) {
return true;
}
}
return false;
}
autoSave() {
console.log("Auto save")
const bgItem = this.bgItem;
if (this.bg) {
bgItem['rect'] = this.bg.getBoundingBox();
} else {
bgItem['rect'] = {x: 0, y: 0, width: Math.round(this.canvasWidth * 100) / 100, height: Math.round(this.canvasHeight * 100) / 100};
}
const hotZoneItemArr = [];
const hotZoneArr = this.hotZoneArr;
for (let i = 0; i < hotZoneArr.length; i++) {
const hotZoneItem = {
index: hotZoneArr[i].index,
};
hotZoneItem['rect'] = hotZoneArr[i].getBoundingBox();
const currentX = hotZoneItem['rect'].x
const currentY = hotZoneItem['rect'].y
hotZoneItem['media'] = {}
hotZoneItem['scale'] = this.scale
hotZoneItem['rect'].x = (Math.round( (currentX - bgItem['rect'].x) * 100) / 100) * this.scale;
hotZoneItem['rect'].y = (Math.round( (currentY - bgItem['rect'].y) * 100) / 100) * this.scale;
hotZoneItem['rect'].width = (Math.round( (hotZoneItem['rect'].width) * 100) / 100) * this.scale;
hotZoneItem['rect'].height = (Math.round( (hotZoneItem['rect'].height) * 100) / 100) * this.scale;
hotZoneItem['media'].image_url = hotZoneArr[i].image_url
hotZoneItem['media'].audio_url = hotZoneArr[i].audio_url?hotZoneArr[i].audio_url:""
hotZoneItem['media'].card_audio_url = hotZoneArr[i].card_audio_url?hotZoneArr[i].card_audio_url:""
hotZoneItemArr.push(hotZoneItem);
}
this.save.emit({bgItem, hotZoneItemArr});
this.hotZoneChanged = false;
}
saveClick() {
console.log("Saved")
this.autoSave()
}
}
<div class="title-config">
<div class="title-wrap">
<div class="row" style="margin: 5px;">
<div class="p-content" style="width:100%">
<div class="p-tool-box d-flex" style="background: #fff;">
<nz-select class="ml-1" style="width: 120px;" [(ngModel)]="__fontFamily"
(ngModelChange)="onChangeFontFamily($event)"
nzPlaceHolder="Font Family"
[nzDropdownMatchSelectWidth]="false"
>
<nz-option [nzValue]="font" [nzLabel]="font" *ngFor="let font of fontFamilyList"></nz-option>
</nz-select>
<nz-select class="ml-1" style="width: 110px;" [(ngModel)]="__fontSize"
(ngModelChange)="onChangeFontSize()"
nzPlaceHolder="Font Size">
<nz-option [nzValue]="i" [nzLabel]="'Size - ' + i" *ngFor="let i of fontSizeRange"></nz-option>
</nz-select>
<div class="p-divider"></div>
<div class="i-tool-font-btn d-flex mr-2">
<div class="position-relative fa-icon" (click)="onChangeBold()">
<div class="fa fa-bold"></div>
</div>
</div>
<div class="i-tool-font-btn d-flex mr-2">
<div class="position-relative fa-icon" (click)="onChangeItalic()">
<div class="fa fa-italic"></div>
</div>
</div>
<div class="i-tool-font-btn d-flex mr-2">
<div class="position-relative fa-icon" (click)="onChangeUnderline()">
<div class="fa fa-underline"></div>
</div>
</div>
<div class="i-tool-font-btn d-flex">
<div class="position-relative fa-icon" (click)="onChangeStrikethrough()">
<div class="fa fa-strikethrough"></div>
</div>
</div>
<div class="p-divider"></div>
<div class="i-tool-font-color d-flex">
<div class="position-relative i-left flex-fill" (click)="onChangeFontColor($event)">
<div class="fa fa-font"></div>
<div class="i-color" [style.background-color]="__fontColor"></div>
</div>
<div class="i-dropdown-menu" nzPlacement="bottom"
nz-popover [(nzVisible)]="isShowFontColorPane" nzTrigger="click"
[nzContent]="colorPane">
<i nz-icon type="down" theme="outline"></i>
</div>
</div>
<div class="p-divider"></div>
<div style="background: #fff;display: block;">
<div class="position-relative" (click)="onChangeStrikethrough()">
<app-audio-recorder [audioUrl]="titleObj && titleObj.audio_url" (audioUploaded)="titleAudioUploaded($event)"></app-audio-recorder>
</div>
</div>
</div>
<div class="width-100 d-flex">
<iframe #titleEl frameborder="0" style="overflow: hidden;width: 100%; height:48px; margin: 0; padding: 0;"></iframe>
</div>
</div>
<ng-template #colorPane>
<div class="p-color-pane" nz-row nzGutter="16" nzType="flex">
<div nz-col class="p-color-item" *ngFor="let color of colorList"
[style.background-color]="color"
(click)="onSelectColor(color)"
>
</div>
</div>
</ng-template>
</div>
<ng-container *ngIf="withIcon">
<div class="row type-row">
课程类型:
<nz-radio-group [(ngModel)]="titleObj && titleObj.type" (ngModelChange)="typeChange($event)">
<label nz-radio nzValue="a">单数课</label>
<label nz-radio nzValue="b">双数课</label>
<label nz-radio nzValue="c">复习课</label>
</nz-radio-group>
</div>
</ng-container>
</div>
<ng-container *ngIf="withIcon">
<div class="title-icons">
<div class="icons-list">
<nz-checkbox-wrapper style="width: 100%;clear:both" (nzOnChange)="iconsChanges($event)">
<div [class]="'icon-item icon-'+i" *ngFor="let i of groupIconsCount[titleObj.type];">
<div class="img-box">
<nz-badge class="icon-badge" [nzCount]="titleObj && titleObj.icons && titleObj.icons.indexOf(i) + 1">
<img [src]="'assets/title-icons/'+titleObj.type+'/icon-'+i+'.png'" alt="">
</nz-badge>
</div>
<label nz-checkbox [nzValue]="i" [ngModel]="titleObj && titleObj.icons && titleObj.icons.indexOf(i) > -1"></label>
</div>
</nz-checkbox-wrapper>
</div>
</div>
</ng-container>
</div>
@import '../../style/common_mixin';
.title-config {
.letter-wrap{
width: 3rem;
flex: 0 0 3rem;
}
.str-wrap{
margin-left: 1rem;flex: 1 1;
}
.type-row{
margin: 0;padding-top: 1rem;
}
.icon-item{
margin-right: 16px;
float: left;
width: 45px;
height: 75px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
.icon-badge{
position: absolute;
top: 0;
right: 0;
}
.img-box{
top: 0;
position: absolute;
width: 45px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
img{
max-width: 100%;
}
}
label{
position: absolute;
bottom: 0;
}
}
}
@mixin tool-btn {
border: 1px solid #ddd;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
outline: none;
border-radius: 6px;
color: #555;
}
.p-title-box {
.p-title {
font-size: 20px;
}
input {
width: 300px;
margin-left: 10px;
}
}
.p-content {
border: 1px solid #ddd;
box-shadow: 0 0 8px #eee;
margin-top: 10px;
}
.p-divider {
border-left: 1px solid #ccc;
margin: 5px 8px;
height: 85%;
}
.p-tool-box {
background-color: #efefef;
padding: 2px;
height: 37px;
display: flex;
align-items: center;
border-bottom: 1px solid #ddd;
// save
.i-tool-save {
@include tool-btn();
color: white;
}
.i-tool-save:disabled {
color: #555;
}
// font-size
.i-tool-font-size {
@include tool-btn();
width: 37px;
& > span {
position: absolute;
top: -5px;
right: 5px;
}
}
.i-tool-font-size:hover {
color: black;
border-color: #bbb;
}
// font-color
.i-tool-font-color, .i-tool-font-btn {
border: 1px solid #ddd;
//padding: 3px 7px;
border-radius: 6px;
width: 45px;
height: 31px;
background-color: white;
color: #555;
::ng-deep > span {
display: flex;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
padding: 3px 7px;
}
.i-left {
.fa-font,.fa-bold,.fa-italic,.fa-strikethrough, .fa-underline {
font-size: 10px;
position: absolute;
color: #555;
left: 8px;
top: 7px;
}
.i-color {
width: 68%;
height: 5px;
background-color: black;
position: absolute;
top: 21px;
left: 5px;
}
}
.i-dropdown-menu {
width: 15px;
font-size: 10px;
border-left: 1px solid #ddd;
display: flex;
align-items: center;
.anticon-down {
transform: scale(0.6);
}
}
}
.i-tool-font-btn{
width: 31px;
}
.fa-icon{
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
// bg-color
.i-tool-bg-color {
@include tool-btn();
padding: 0 9px;
::ng-deep > span {
display: flex;
align-items: center;
}
.i-color {
display: block;
width: 16px;
height: 16px;
background-color: white;
margin-left: 10px;
}
}
// horizontal-center
.i-tool-horizontal-center {
@include tool-btn();
width: 37px;
}
}
.p-box {
width: 1280px;
height: 720px;
left: 0;
top: 0;
transform-origin: top left;
overflow: hidden;
}
.p-sentence {
@include k-no-select();
}
.p-animation-index-box {
.i-animation-index {
position: absolute;
font-size: 34px;
font-family: Arial;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 50px;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
background-color: darkorange;
color: white;
}
}
.p-edit {
background-color: white;
border: 1px solid black;
padding: 2px 15px;
}
.p-selected {
border: 1px solid darkorange;
box-shadow: 0 0 8px #ddd;
}
.p-tool-item-disable {
color: rgba(0, 0, 0, .25) !important;
background-color: transparent !important;
pointer-events: none;
}
// -----------
.p-color-pane {
width: 80px;
.p-color-item {
width: 17px;
height: 17px;
cursor: pointer;
margin: 4px;
border: 1px solid #bbb;
}
.p-color-item-active {
border: 1px solid white;
transform: scale(1.1);
}
}
.p-user-guide {
font-size: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.p-animation-box {
width: 400px;
background-color: #efefef;
border-left: 1px solid #ddd;
padding: 10px;
text-align: center;
.p-animation-label {
z-index: 1;
position: relative;
}
::ng-deep .ant-radio-button-wrapper {
padding: 0 10px;
@include k-no-select();
}
.i-toolbox {
& > div {
font-size: 20px;
cursor: pointer;
color: #555;
padding: 0 3px;
margin: 0 5px;
}
& > div:hover {
color: #1585ff;
transform: scale(1.1);
}
}
.p-animation-list {
background-color: white;
border-radius: 10px;
height: calc(100% - 86px);
overflow-y: auto;
.p-animation-item {
padding: 0 20px;
background-color: aliceblue;
margin-top: 6px;
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
cursor: pointer;
text-align: left;
display: flex;
@include k-no-select();
}
.i-active {
background-color: antiquewhite;
}
}
}
import {
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
@Component({
selector: 'app-lesson-title-config',
templateUrl: './lesson-title-config.component.html',
styleUrls: ['./lesson-title-config.component.scss']
})
export class LessonTitleConfigComponent implements OnInit, OnChanges, OnDestroy, OnChanges {
fontFamilyList = [
'Arial',
'ARBLI'
];
colorList = [
'#111111',
'#ffffff',
'#595959',
'#0075c2',
'#c61c1e',
'#9cbc3a'
];
MIN_FONT_SIZE = 1;
MAX_FONT_SIZE = 7;
isShowFontColorPane = false;
isShowBGColorPane = false;
fontSizeRange: number[];
editorContent = '';
__fontFamily = 'Arial';
__fontColor = '';
__fontSize = 3;
loopCnt = 0;
maxLoops = 20;
groupIconsCount = {
a: Array.from(Array(11).keys()),
b: Array.from(Array(8).keys()),
c: Array.from(Array(8).keys()),
};
prevIcons = [];
prevType = '';
@ViewChild('titleEl') titleEl: ElementRef;
titleEW = null;
@Input()
titleObj = {
type: 'a',
content: '',
icons: [],
audio_url: ''
};
@Input()
withIcon = true;
@Output()
titleUpdated = new EventEmitter();
constructor() {
this.fontSizeRange = [];
for (let i = this.MIN_FONT_SIZE; i <= this.MAX_FONT_SIZE; ++i) {
this.fontSizeRange.push(i);
}
this.__fontSize = 3;
this.__fontColor = this.colorList[0];
}
ngOnChanges(vars) {
if (!vars.titleObj.previousValue) {// 初始化,内容是空
return;
}
let defObj = this.titleObj;
if (!vars.titleObj.currentValue) {
defObj = {
type: 'a',
content: '',
icons: [],
audio_url: ''
};
} else {
defObj = vars.titleObj.currentValue;
}
this.titleObj.icons = defObj.icons || [];
this.titleObj.type = defObj.type || 'a';
this.titleObj.content = defObj.content || '';
this.titleObj.audio_url = defObj.audio_url || '';
this.titleEW.document.body.innerHTML = this.titleObj.content;
}
ngOnInit() {
if (!this.titleObj) {
this.titleObj = {
type: 'a',
content: '',
icons: [],
audio_url: ''
};
}
this.titleObj.icons = this.titleObj.icons || [];
this.titleObj.type = this.titleObj.type || 'a';
this.titleObj.content = this.titleObj.content || '';
this.titleObj.audio_url = this.titleObj.audio_url || '';
this.editorContent = `<html lang="en"><head><meta charset="utf-8">
<meta name="viewport"
content="width=device-width,initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no"/>
</head>
<body style="height:48px;overflow: hidden;margin: 0;padding: 0 .5rem;background: #FFF;line-height: 48px;">
${this.titleObj.content}
</body>
</html>`;
this.titleEW = this.titleEl.nativeElement.contentWindow;
const tdoc = this.titleEW.document;
tdoc.designMode = "on";
tdoc.open('text/html', 'replace');
tdoc.write(this.editorContent);
tdoc.close();
tdoc.addEventListener("keypress", this.keyPress, true);
tdoc.addEventListener("blur", () => {
if (this.titleObj.content === this.titleEW.document.body.innerHTML.trim()) {
return;
}
this.shouldSave();
}, true);
}
htmlEncode(text) {
if (!text) {
return '';
}
return text.replace(/\&/ig, '&amp;')
.replace(/\</ig, '&lt;')
.replace(/\>/ig, '&gt;')
.replace(/\"/ig, '&quot;');
}
htmlDecode(text) {
if (!text) {
return '';
}
return text.replace(/\&amp\;/ig, '&')
.replace(/\&lt\;/ig, '<')
.replace(/\&gt\;/ig, '>')
.replace(/\&quot\;/ig, '"');
}
ngOnDestroy(): void {
}
iconsChanges(val) {
let a = this.titleObj.icons;
let b = val;
if (a.length > b.length) {
const diff = a.filter(x => !b.includes(x));
const ti = [...this.titleObj.icons];
for (let i = 0; i < diff.length; i++) {
const d = diff[i];
const idx = ti.indexOf(d);
ti.splice(idx, 1);
}
this.titleObj.icons = ti;
} else {
const diff = b.filter(x => !a.includes(x));
this.titleObj.icons = [...this.titleObj.icons, ...diff];
}
this.shouldSave();
}
typeChange(val) {
this.titleObj.icons = [];
this.shouldSave();
}
keyPress(evt) {
try {
if (evt.charCode === 13) {
evt.preventDefault();
evt.stopPropagation();
return;
}
if (evt.ctrlKey) {
const key = String.fromCharCode(evt.charCode).toLowerCase();
let cmd = '';
switch (key) {
case 'b': cmd = "bold"; break;
case 'i': cmd = "italic"; break;
case 'u': cmd = "underline"; break;
}
if (cmd) {
this.execEditorCommand(cmd);
// stop the event bubble
evt.preventDefault();
evt.stopPropagation();
}
}
} catch (e) {
console.log(1, e);
alert(e);
}
}
execEditorCommand(command, option?: any) {
try {
this.titleEW.focus();
this.titleEW.document.execCommand(command, false, option);
this.loopCnt = 0;
return false;
} catch (e) {
alert(e);
if (this.loopCnt < this.maxLoops) {
setTimeout(() => {
this.execEditorCommand(command, option);
}, 100);
this.loopCnt += 1;
} else {
alert("Error executing command.");
}
}
}
onSelectColor(color) {
this.execEditorCommand('forecolor', color);
this.__fontColor = color;
}
onChangeFontColor(val) {
this.execEditorCommand('forecolor', this.__fontColor);
}
onChangeFontFamily(font) {
this.execEditorCommand('fontname', font);
}
onChangeFontSize(size?: any) {
if (size) {
size += this.__fontSize;
} else {
size = this.__fontSize;
}
size = Math.max(this.MIN_FONT_SIZE, size);
size = Math.min(this.MAX_FONT_SIZE, size);
this.execEditorCommand('fontsize', size);
}
onChangeBold() {
this.execEditorCommand('bold');
}
onChangeItalic() {
this.execEditorCommand('italic');
}
onChangeUnderline() {
this.execEditorCommand('underline');
}
onChangeStrikethrough() {
this.execEditorCommand('strikethrough');
}
titleAudioUploaded(res) {
this.titleObj.audio_url = res.url;
this.titleUpdated.emit(this.titleObj);
}
shouldSave = () => {
console.log('title shouldSave');
this.titleObj.content = this.titleEW.document.body.innerHTML.trim();
this.titleUpdated.emit(this.titleObj);
}
}
<div class="cmp-player-content-wrapper" #wrapperEl>
<div class="cmp-cnt-box">
<img [src]="'assets/'+ratio+'.png'" alt="">
<div class="cmp-cnt-main">
<ng-content></ng-content>
</div>
</div>
</div>
\ No newline at end of file
@import '../../style/common_mixin';
.cmp-player-content-wrapper{
max-height: 100%;
display: block;
position: relative;
height: 100%;
.cmp-cnt-box{
display: inline-block;
max-width: 100%;
max-height: 100%;
width: auto;
height: 100%;
position: relative;
img{
height: 100%;
width: auto;
}
}
.cmp-cnt-main{
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
}
}
[style*="--aspect-ratio"] > :first-child {
width: 100%;
}
[style*="--aspect-ratio"] > img {
height: auto;
}
@supports (--custom:property) {
[style*="--aspect-ratio"] {
position: relative;
}
[style*="--aspect-ratio"]::before {
content: "";
display: block;
padding-bottom: calc(100% / (var(--aspect-ratio)));
}
[style*="--aspect-ratio"] > :first-child {
position: absolute;
top: 0;
left: 0;
height: 100%;
}
}
import {
AfterViewInit,
Component,
ElementRef,
Input,
OnChanges,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
@Component({
selector: 'app-player-content-wrapper',
templateUrl: './player-content-wrapper.component.html',
styleUrls: ['./player-content-wrapper.component.scss']
})
export class PlayerContentWrapperComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit {
@ViewChild('wrapperEl') wrapperEl: ElementRef;
// // aspect ratio?
@Input() ratio;
_w: string;
_h: string;
constructor() {
if (window.innerHeight < window.innerWidth) {
this._h = '100%';
this._w = 'auto';
} else {
this._w = '100%';
this._h = 'auto';
}
}
ngOnInit() {
if (!this.ratio) {
this.ratio = '20-9';
}
}
ngOnChanges() {
}
ngOnDestroy(): void {
}
ngAfterViewInit() {
}
}
<div class="up-video" style="display: flex;">
<!--<nz-upload class="" [nzDisabled]="!showUploadBtn"-->
<!--[nzShowUploadList]="false"-->
<!--[nzBeforeUpload]="beforeUpload"-->
<!--nzAccept = ".mp4"-->
<!--style="margin-right: 1rem">-->
<nz-upload class="p-image-uploader" [nzDisabled]="uploading"
[nzShowUploadList]="false"
nzAccept = ".mp4"
[nzAction]="uploadUrl"
[nzData]="uploadData"
(nzChange)="handleChange($event)"
style="margin-right: 1rem">
<button type="button" nz-button nzType="default" *ngIf="showUploadBtn" [disabled]="uploading"
[nzLoading]="uploading" >
<i nz-icon type="plus" theme="outline"></i>
<span>{{ uploading ? 'Uploading' : 'Select Video' }}</span>
<!--<span>Select Video</span>-->
</button>
</nz-upload>
<!--<button nz-button nzType="primary" *ngIf="showUploadBtn"-->
<!--style="margin-right: 1rem"-->
<!--type="button"-->
<!--[nzLoading]="uploading"-->
<!--(click)="handleUpload()"-->
<!--[disabled]="!videoFile || uploading">-->
<!--<i nz-icon type="upload" theme="outline"></i>-->
<!--<span>{{ uploading ? 'Uploading' : 'Start Upload' }}</span>-->
<!--</button>-->
<!--
<button type="button"
(click)="extraCover()"
nz-button nzType="default"
[disabled]=" !(videoUrl && videoUrl.constructor.name === 'String')">设置封面</button>
-->
<!--
<nz-divider></nz-divider>
<div [style.display]="!uploading?'none':''" style="position:relative">
<div style="width: calc( 100% - 20px);">
<nz-progress [nzPercent]="progress">
</nz-progress>
</div>
<i (click)="cancelUpload()" class="anticon anticon-close-circle" style="position: absolute;top: 50%;right: 0;transform: translateY(-50%);cursor: pointer"></i>
</div>
-->
</div>
<div class="p-box d-flex align-items-center p-video-uploader" style="top: 20px;">
<div class="p-upload-icon" *ngIf="!showUploadBtn && !videoUrl && !uploading">
<i nz-icon type="upload" theme="outline"></i>
<div class="m-3"></div>
<span>Click here to upload video</span>
<div class="mt-5 p-progress-bar" *ngIf="uploading">
<div class="p-progress-bg" [style.width]="progress*0.2+'rem'"></div>
<div class="p-progress-value">{{progress}}%</div>
</div>
</div>
<div class="p-upload-progress-bg dddd " *ngIf="uploading"
[ngClass]="{'smart-bar': showUploadBtn}" >
<div class="i-bg" [style.width]="progress+'%'"></div>
<div class="i-text">
<fa name="cloud-upload"></fa>
Uploading...
</div>
</div>
<div class="p-upload-check-bg" *ngIf="checking">
<div class="i-bg" [style.width]="progress+'%'"></div>
<div class="i-text">
<fa name="cloud-upload"></fa>
<i nz-icon type="loading" theme="outline"></i>Checking...
</div>
</div>
<div class="p-preview" *ngIf="!showUploadBtn && !uploading && videoUrl " >
<!--<video crossorigin="anonymous" [src]="videoUrl" controls #videoNode></video>-->
<video [src]="safeVideoUrl(videoUrl)" controls #videoNode (loadedmetadata)="videoLoadedMetaData()"></video>
</div>
</div>
<div [style.display]="!checkVideoExists?'none':''">
<span><i nz-icon type="loading" theme="outline"></i> checking file to upload</span>
</div>
@import '../../style/common_mixin';
.p-video-uploader {
position: relative;
display: block;
width: 100%;
height: 0;
padding-bottom: 56.25%;
.p-box {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border: 2px dashed #ddd;
//border-radius: 0.5rem;
background-color: #fafafa;
text-align: center;
color: #aaa;
.p-upload-icon {
text-align: center;
margin: auto;
.anticon-upload {
color: #888;
font-size: 5rem;
}
.p-progress-bar {
position: relative;
width: 20rem;
height: 1.5rem;
border: 1px solid #ccc;
border-radius: 1rem;
.p-progress-bg {
background-color: #1890ff;
border-radius: 1rem;
height: 100%;
}
.p-progress-value {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-shadow: 0 0 4px #000;
color: white;
text-align: center;
font-size: 0.9rem;
line-height: 1.5rem;
}
}
}
.p-preview {
width: 100%;
height: 100%;
//background-image: url("https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png");
video{
max-height: 100%;
max-width: 100%;
}
}
}
}
.p-btn-delete {
position: absolute;
right: -0.5rem;
top: -0.5rem;
width: 2rem;
height: 2rem;
border: 0.2rem solid white;
border-radius: 50%;
font-size: 1.2rem;
background-color: #fb781a;
color: white;
text-align: center;
}
.p-upload-progress-bg, .p-upload-check-bg {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
& .i-text {
position: absolute;
text-align: center;
color: white;
text-shadow: 0 0 2px rgba(0, 0, 0, .85);
}
& .i-bg {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: #1890ff;
//border-radius: 0.5rem 0 0 0.5rem;
}
}
.p-upload-progress-bg.smart-bar{
height: 50px!important;
margin-top: 20px!important;
}
.p-upload-check-bg {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
& .i-text {
position: absolute;
text-align: center;
color: white;
text-shadow: 0 0 2px rgba(0, 0, 0, .85);
}
& .i-bg {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: #17a2b8;
//border-radius: 0.5rem 0 0 0.5rem;
}
}
//:host ::ng-deep .ant-upload {
// display: block;
//}
import {Component, ElementRef, EventEmitter, Input, OnChanges, OnDestroy, Output, SecurityContext, ViewChild} from '@angular/core';
import {NzMessageService, UploadFile} from 'ng-zorro-antd';
import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
@Component({
selector: 'app-upload-video',
templateUrl: './upload-video.component.html',
styleUrls: ['./upload-video.component.scss']
})
export class UploadVideoComponent implements OnChanges, OnDestroy {
uploading = false;
checking = false;
checkVideoExists = false;
uploadClicked = false;
@Input()
videoFile = null;
uploaderInst = null;
progress = 0;
// @Input()
// setCovering = false;
@Input()
videoUrl = '';
@ViewChild('videoNode')
videoNode: ElementRef;
// @Input()
// extractCoverFunc = null;
@Output()
videoUploaded = new EventEmitter();
@Output()
videoUploadFailure = new EventEmitter();
@Output()
extractVideoCover = new EventEmitter();
@Input()
showUploadBtn = true;
@Input()
item: any;
// videoItem = null;
uploadUrl = (<any> window).courseware.uploadUrl();
uploadData = (<any> window).courseware.uploadData();
constructor(private nzMessageService: NzMessageService,
private sanitization: DomSanitizer
// private cwService: _coursewareService,
// private resService: ResourceService
) {
this.uploading = false;
this.videoFile = null;
}
ngOnChanges() {
// if (!this.videoFile || this.showUploadBtn) {
// return;
// }
// this.beforeUpload(this.videoFile);
// this.handleUpload();
}
ngOnDestroy(): void {
URL.revokeObjectURL(this.videoUrl);
}
safeVideoUrl(url) {
console.log(url)
return this.sanitization.bypassSecurityTrustResourceUrl(url); // `${url}`;
}
videoLoadedMetaData() {
}
handleChange(info: { type: string, file: UploadFile, event: any }): void {
console.log('info:' , info);
switch (info.type) {
case 'start':
// this.beforeUpload(info.file);
if (!this.checkSelectFile(info.file)) {
return;
}
this.uploading = true;
this.progress = 0;
break;
case 'success':
this.uploadSuccess(info.file);
// this.beforeUpload(info.file);
// this.uploadSuccess(info.file);
break;
case 'progress':
this.progress = parseInt(info.event.percent, 10);
this.doProgress(this.progress);
break;
}
}
checkSelectFile(file) {
if (!file.lastModified) {
return false;
}
const delta = 500;
const isOverSize = (file.size / 1024 / 1024) < delta;
if (!isOverSize) {
this.nzMessageService.error(`video must smaller than ${delta}MB!`);
return false;
}
return true;
}
checkHashFinish = (hash) => {
this.checking = false;
this.uploading = true;
}
uploadSuccess = (file) => {
this.nzMessageService.info('Upload Success');
// this.updateStatus(false);
this.uploading = false;
this.videoFile = null;
// this.updateSource(url);
this.videoUrl = file.response.url;
// console.log(this.picUrl)
// this.uploadFinished(url);
// if (!inOSS) {
const vid = document.createElement('video');
vid.addEventListener('loadedmetadata', () => {
const height = vid.videoHeight;
const width = vid.videoWidth;
let duration = vid.duration;
if (duration) {
duration = duration * 1000;
}
file['height'] = height;
file['width'] = width;
file['duration'] = duration;
vid.preload = 'none';
vid.src = '';
vid.remove();
this.videoUploaded.emit(file.response);
// this.resService.updateVideo(id, {width, height, duration}).then( () => {
// this.videoUploaded.emit({res_id: id, id, name, hash, url});
// });
});
vid.src = file.response.url;
// } else {
// this.videoUploaded.emit(file.response);
// }
}
uploadFailure = (err, file) => {
this.uploading = false;
if (err.name && err.name === 'cancel') {
return;
}
console.log(err);
this.nzMessageService.error('Upload Error ' + err.message);
this.videoUploadFailure.emit(file);
}
doProgress = (p) => {
if (p > 1) {
p = 1;
}
if (p < 0) {
p = 0;
}
// console.log(Math.floor(p * 100));
this.progress = Math.floor(p * 100);
}
}
......@@ -31,67 +31,9 @@
}
@font-face
{
font-family: 'BRLNSDB';
src: url("../../assets/font/BRLNSDB.TTF") ;
}
@font-face
{
font-family: 'RoundedBold';
src: url("../../assets/font/ArialRoundedBold.otf") ;
}
@font-face{
font-family: 'BRLNSB_1';
src: url("../../assets/font/BerlinSansFB/BRLNSB_1.TTF") ;
}
@font-face{
font-family: 'BerlinSansFBDemi-Bold';
src: url("../../assets/font/BerlinSansFB/BRLNSDB_1.TTF") ;
}
@font-face{
font-family: 'BRLNSR_1';
src: url("../../assets/font/BerlinSansFB/BRLNSR_1.TTF") ;
}
@font-face{
font-family: 'GOTHIC_1';
src: url("../../assets/font/CenturyGothic/GOTHIC_1.TTF") ;
}
@font-face{
font-family: 'GOTHICB_1';
src: url("../../assets/font/CenturyGothic/GOTHICB_1.TTF") ;
}
@font-face{
font-family: 'GOTHICBI_1';
src: url("../../assets/font/CenturyGothic/GOTHICBI_1.TTF") ;
}
@font-face{
font-family: 'GOTHICI_1';
src: url("../../assets/font/CenturyGothic/GOTHICI_1.TTF") ;
}
@font-face{
font-family: 'MMTextBook';
src: url("../../assets/font/MMTextBook/MMTextBook.otf") ;
}
@font-face{
font-family: 'MMTextBook-Bold';
src: url("../../assets/font/MMTextBook/MMTextBook-Bold.otf") ;
}
@font-face{
font-family: 'MMTextBook-BoldItalic';
src: url("../../assets/font/MMTextBook/MMTextBook-BoldItalic.otf") ;
}
@font-face{
font-family: 'MMTextBook-Italic';
src: url("../../assets/font/MMTextBook/MMTextBook-Italic.otf") ;
}
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