Commit 32b46ab0 authored by liujiaxin's avatar liujiaxin

1111

parent 8a51c964
...@@ -30,7 +30,10 @@ ...@@ -30,7 +30,10 @@
], ],
"styles": [ "styles": [
"./node_modules/ng-zorro-antd/ng-zorro-antd.min.css", "./node_modules/ng-zorro-antd/ng-zorro-antd.min.css",
"src/styles.css" "./node_modules/font-awesome/css/font-awesome.css",
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"./node_modules/animate.css/animate.min.css",
"src/styles.scss"
], ],
"scripts": [] "scripts": []
}, },
......
...@@ -24,6 +24,9 @@ import {AudioRecorderComponent} from './common/audio-recorder/audio-recorder.com ...@@ -24,6 +24,9 @@ import {AudioRecorderComponent} from './common/audio-recorder/audio-recorder.com
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { fas } from '@fortawesome/free-solid-svg-icons'; import { fas } from '@fortawesome/free-solid-svg-icons';
import { far } from '@fortawesome/free-regular-svg-icons'; import { far } from '@fortawesome/free-regular-svg-icons';
import { CustomStyleDirective } from './directives/custom-style.directive';
import { BrLineBreakerPipe } from './pipes/br-line-breaker.pipe';
import { ResizeDirective } from './directives/resize.directive';
registerLocaleData(zh); registerLocaleData(zh);
...@@ -40,7 +43,9 @@ registerLocaleData(zh); ...@@ -40,7 +43,9 @@ registerLocaleData(zh);
TimePipe, TimePipe,
UploadVideoComponent, UploadVideoComponent,
CustomHotZoneComponent, CustomHotZoneComponent,
CustomStyleDirective,
BrLineBreakerPipe,
ResizeDirective,
PlayerContentWrapperComponent PlayerContentWrapperComponent
], ],
......
import {Directive, ElementRef, Input, OnChanges, SimpleChanges} from '@angular/core';
import * as _ from 'lodash';
@Directive({
selector: '[appCustomStyle]'
})
export class CustomStyleDirective implements OnChanges {
@Input()
customStyle;
@Input()
customStyleDirtyCount;
constructor(private el: ElementRef) {
}
ngOnChanges(changes: SimpleChanges) {
const dom = this.el.nativeElement;
// dom.style.fontFamily = this.customStyle.fontFamily;
// dom.style.color = this.customStyle.color;
_.assign(dom.style, this.customStyle);
// Object.entries(this.customStyle).forEach( ([key, value]) => {
// dom.style[key] = value;
// });
dom.style.fontSize = (this.customStyle.fontSize * 10) + 'px';
// dom.style.transform = `translate(${this.customStyle.position.left}px, ${this.customStyle.position.top}px)`;
dom.style.left = this.customStyle.position.left + 'px';
dom.style.top = this.customStyle.position.top + 'px';
}
}
import {Directive, ElementRef, EventEmitter, Output} from '@angular/core';
import {ResizeSensor} from 'css-element-queries';
import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
@Directive({
selector: '[appResize]'
})
export class ResizeDirective {
@Output()
resize = new EventEmitter();
debounce = new Subject();
constructor(private el: ElementRef) {
this.debounce
.pipe(debounceTime(100))
.subscribe(rect => {
this.resize.emit(rect);
});
const sensor = new ResizeSensor(this.el.nativeElement, (event) => {
const {clientWidth, clientHeight} = this.el.nativeElement;
this.debounce.next({clientWidth, clientHeight});
});
}
}
@import '../style/common_mixin.css';
.model-content {
width: 100%;
height: 100%;
}
<div class="p-title-box">
<span class="p-title">Thumb Title</span>
<input nz-input (change)="extraData.changeDirtyCount=extraData.changeDirtyCount+1"
[(ngModel)]="item.contentObj.title"
placeholder="please enter thumb title"
/>
</div>
<div class="p-content">
<div class="p-tool-box d-flex">
<!------------------------------------------------------------------------>
<nz-select class="ml-1" style="width: 120px;" [nzDisabled]="!curItem" [(ngModel)]="fontFamily"
(ngModelChange)="onChangeFontFamily()"
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;" [nzDisabled]="!curItem" [(ngModel)]="fontSize"
(ngModelChange)="onChangeFontSize()"
nzPlaceHolder="Font Size">
<nz-option [nzValue]="i" [nzLabel]="'Size - ' + i" *ngFor="let i of fontSizeRange"></nz-option>
</nz-select>
<!------------------------------------------------------------------------>
<button class="position-relative ml-1 i-tool-font-size" nz-button [disabled]="!curItem"
(click)="onChangeFontSize(-1)">
<i class="fa fa-font"></i>
<span>-</span>
</button>
<!------------------------------------------------------------------------>
<button class="position-relative ml-1 i-tool-font-size" nz-button [disabled]="!curItem"
(click)="onChangeFontSize(1)">
<i class="fa fa-font"></i>
<span style="font-size: 13px; top: -2px;">+</span>
</button>
<div class="p-divider"></div>
<!------------------------------------------------------------------------>
<div class="i-tool-font-color d-flex" [class.p-tool-item-disable]="!curItem">
<div class="position-relative i-left flex-fill" (click)="onChangeFontColor()">
<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 class="anticon anticon-down"></i>
</div>
</div>
<!------------------------------------------------------------------------>
<button class="ml-2 i-tool-horizontal-center" nz-button nz-tooltip nzTitle="Horizontal Center"
[disabled]="!curItem"
(click)="onBtnMoveToCenter(true, false)"
>
<i class="fa fa-align-center"></i>
</button>
<div class="p-divider"></div>
<!------------------------------------------------------------------------>
<button class="ml-2 i-tool-delete" nz-button nzType="danger"
[disabled]="!curItem"
nz-popconfirm nzTitle="Delete ?" (nzOnConfirm)="onBtnDelete()"
>
<i class="anticon anticon-minus-circle"></i>
</button>
<div class="p-divider"></div>
<!------------------------------------------------------------------------>
<button class="ml-2 i-tool-bg-color" nz-button nzPlacement="bottom"
nz-popover [(nzVisible)]="isShowBGColorPane" nzTrigger="click"
[nzContent]="colorPane">
BG <span class="i-color" [style.background-color]="item.contentObj.bgColor"></span>
</button>
<!------------------------------------------------------------------------>
<button class="ml-2 i-tool-animation" nz-button
(click)="onBtnShowAnimationPane()"
[style.color]="isShowAnimationPane ? '#1585ff' : ''"
[style.border-color]="isShowAnimationPane ? '#1585ff' : ''"
>
<i class="fa fa-fighter-jet"></i>
</button>
<!------------------------------------------------------------------------>
<div class="flex-fill"></div>
<!------------------------------------------------------------------------>
<button class="ml-2 i-tool-restore" nz-button nzType="danger"
[disabled]="extraData.changeDirtyCount==0"
nz-popconfirm nzTitle="Remove ALL changes ?" (nzOnConfirm)="onBtnRestore()"
>
<i class="fa fa-reply"></i>
<span class="ml-1" *ngIf="extraData.changeDirtyCount > 0">Restore</span>
</button>
<!------------------------------------------------------------------------>
<div class="flex-fill"></div>
<!------------------------------------------------------------------------>
<button class="ml-1 i-tool-save" nz-button nzType="primary"
(click)="onBtnSave()"
[disabled]="extraData.changeDirtyCount==0">
<i class="anticon anticon-inbox"></i>
<span *ngIf="extraData.changeDirtyCount > 0">Save</span>
</button>
</div>
<div class="model-content"> <div class="width-100 d-flex">
<div class="k-16-9" appResize (resize)="resize($event)">
<div class="position-absolute p-box" #box tabindex="1"
<div style="position: absolute; left: 200px; top: 100px; width: 800px;"> (click)="onClickBox()"
(dblclick)="onNewSentence($event)"
<input type="text" nz-input [(ngModel)]="item.text" (blur)="save()"> (mousemove)="onMouseMove($event)"
(mouseup)="onMouseUp($event)"
<app-upload-image-with-preview (keydown.ArrowUp)="onMoveSentence(0, -10, $event)"
[picUrl]="item.pic_url" (keydown.ArrowDown)="onMoveSentence(0, 10, $event)"
(imageUploaded)="onImageUploadSuccess($event, 'pic_url')" (keydown.ArrowLeft)="onMoveSentence(-10, 0, $event)"
></app-upload-image-with-preview> (keydown.ArrowRight)="onMoveSentence(10, 0, $event)"
[style.background-color]="item.contentObj.bgColor"
>
<div class="position-absolute p-sentence"
*ngFor="let item of item.contentObj.items; index as i;"
#list
[class.p-selected]="curItem===item"
appCustomStyle [customStyle]="item"
[customStyleDirtyCount]="extraData.changeDirtyCount"
(blur)="onBlur(item, $event.target);"
(dblclick)="onEdit(item, $event)"
(click)="onSelect(item, $event)"
(mousedown)="onMouseDown(item, $event)"
[innerHTML]="item.text | brLineBreaker"
>
</div>
<app-audio-recorder <div class="p-animation-index-box" *ngIf="isShowAnimationPane">
[audioUrl]="item.audio_url" <ng-container *ngFor="let item of item.contentObj.items; index as i;">
(audioUploaded)="onAudioUploadSuccess($event, 'audio_url')" <div class="i-animation-index"
></app-audio-recorder> *ngIf="isShowAnimationOrder && getIndexOfAnimationByItem(item) > -1"
<app-custom-hot-zone></app-custom-hot-zone> [style.left]="item.position.left - item.fontSize*8 - 20 + 'px'"
<app-upload-video></app-upload-video> [style.top]="item.position.top + item.fontSize*10/2 + 'px'"
<app-lesson-title-config></app-lesson-title-config> >
{{getIndexOfAnimationByItem(item) + 1}}
</div>
</ng-container>
</div>
<div class="position-absolute k-full-fill p-user-guide"
*ngIf="item.contentObj.items.length === 0">
Double click on the board to create a text
</div>
</div>
</div>
<div *ngIf="isShowAnimationPane" class="p-animation-box">
<nz-radio-group class="p-animation-label" [(ngModel)]="curAnimationName">
<label nz-radio-button [nzValue]="item.value"
(click)="onSetAnimation(item)"
*ngFor="let item of AnimationType">
<span>{{item.name}}</span>
</label>
</nz-radio-group>
<div class="animated-demo-text infinite {{curAnimationName}}">animation demo</div>
<div class="i-toolbox d-flex">
<div class="flex-fill"></div>
<div (click)="onBtnToggleDisplayAnimationOrder()">
<i class="anticon anticon-eye" [style.color]="isShowAnimationOrder ? '' : 'red'"></i>
</div>
<div (click)="onBtnMoveAnimation(-1)">
<i class="anticon anticon-arrow-up"></i>
</div>
<div (click)="onBtnMoveAnimation(1)">
<i class="anticon anticon-arrow-down"></i>
</div>
<div (click)="onBtnRemoveAnimation()">
<i class="anticon anticon-close"></i>
</div>
</div>
<div class="p-animation-list">
<div class="p-animation-item" [class.i-active]="selectedAnimation===item"
(click)="onClickAnimation(item)"
*ngFor="let item of animations; index as i"
>
<div>{{i + 1}}.</div>
<div class="flex-fill text-center"> {{item.name}}</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
\ No newline at end of file <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)="onChangeColor(color)"
>
</div>
</div>
</ng-template>
@import '../style/mixin';
@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 {
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 {
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);
}
}
}
// 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;
}
}
// animation
.i-tool-animation {
}
// restore
.i-tool-restore {
}
// horizontal-center
.i-tool-horizontal-center {
@include tool-btn();
width: 37px;
}
// delete
.i-tool-delete {
}
}
.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: 105px;
.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, EventEmitter, Input, OnDestroy, OnChanges, OnInit, Output, ApplicationRef, ChangeDetectorRef} from '@angular/core'; import {
Component,
EventEmitter,
Input,
OnDestroy,
OnChanges,
OnInit,
Output,
ApplicationRef,
ChangeDetectorRef,
ViewChild, ViewChildren
} from '@angular/core';
import * as _ from 'lodash';
import { NzMessageService } from 'ng-zorro-antd';
const AnimationType = [
{
name: 'Slide',
value: 'animated fadeInRight'
},
{
name: 'Fade',
value: 'animated fadeInDown'
},
{
name: 'Zoom',
value: 'animated zoomInUp'
},
{
name: 'Jack',
value: 'animated rotateIn' // jackInTheBox
},
{
name: 'Roll',
value: 'animated rollIn'
}
];
@Component({ @Component({
selector: 'app-form', selector: 'app-form',
templateUrl: './form.component.html', templateUrl: './form.component.html',
styleUrls: ['./form.component.css'] styleUrls: ['./form.component.scss']
}) })
export class FormComponent implements OnInit, OnChanges, OnDestroy { export class FormComponent implements OnInit, OnChanges, OnDestroy {
// 储存数据用 // 储存数据用
saveKey = "test_0011"; saveKey = 'ww_text';
// 储存对象 // 储存对象
item; item;
@ViewChild('box', {static: true})
box;
@ViewChildren('list')
list;
curDom;
itemBackup: any;
isMovingItem = false;
ratio: number;
moveOffset: any;
fontFamilyList: string[];
fontSizeRange: number[];
colorList: string[];
MIN_FONT_SIZE = 1;
MAX_FONT_SIZE = 12;
isShowFontColorPane = false;
isShowBGColorPane = false;
isShowAnimationPane = false;
isShowAnimationOrder = true;
fontFamily: string;
bgColor: string;
fontSize: number;
fontColor = 'black';
position: any;
curAnimationName: string;
animations: any[];
_selectedAnimation: any;
extraData = {
changeDirtyCount: 0
};
_curItem;
set curItem(value) {
let changed = false;
if (_.get(value, 'id') !== _.get(this._curItem, 'id')) {
changed = true;
}
this._curItem = value;
if (changed) {
this.onCurItemChanged();
}
}
get curItem() {
return this._curItem;
}
set selectedAnimation(value) {
let changed = false;
if (_.get(value, 'id') !== _.get(this._selectedAnimation, 'id')) {
changed = true;
}
this._selectedAnimation = value;
if (changed) {
this.onSelectedAnimationChanged();
}
}
constructor(private appRef: ApplicationRef,private changeDetectorRef: ChangeDetectorRef) { get selectedAnimation() {
return this._selectedAnimation;
}
AnimationType = AnimationType;
constructor(private appRef: ApplicationRef,
private nzMessageService: NzMessageService,
private changeDetectorRef: ChangeDetectorRef) {
this.fontSizeRange = [];
for (let i = this.MIN_FONT_SIZE; i <= this.MAX_FONT_SIZE; ++i) {
this.fontSizeRange.push(i);
}
const color_black = '#111111';
const color_gray = '#aaaaaa';
const color_silver = '#dddddd';
const color_navy = '#001f3f';
const color_blue = '#0074d9';
const color_aqua = '#7fdbff';
const color_teal = '#39cccc';
const color_olive = '#3d9970';
const color_green = '#2ecc40';
const color_lime = '#01ff70';
const color_yellow = '#ffdc00';
const color_orange = '#ff851b';
const color_red = '#ff4136';
const color_maroon = '#85144b';
const color_fuchsia = '#f012be';
const color_purple = '#b10dc9';
// Ken 2018-10-30 14:37 宁波那边要加5个颜色
this.colorList = [
color_black,
'white',
'#595959',
color_gray,
color_silver,
color_navy,
color_blue,
'blue',
color_aqua,
color_teal,
'#64c0bd',
color_olive,
'#72ac4d',
color_green,
color_lime,
'yellow',
color_yellow,
color_orange,
'#f45970',
color_red,
'#be0712',
color_maroon,
color_fuchsia,
color_purple
];
this.fontFamilyList = [
'Arial',
'monospace',
'ChalkboardSE-Regular'
];
} }
init() {
this.curItem = null;
this.extraData.changeDirtyCount = 0;
this.isMovingItem = false;
this.item.contentObj.items = this.item.contentObj.items || [];
// ver 区分数据
this.item.contentObj.ver = this.item.contentObj.ver || 1;
this.animations = this.item.contentObj.animations = this.item.contentObj.animations || [];
// Ken 2018-09-14 09:33 为item添加id属性, 对于旧数据, 要自动添加此属性
if (!_.get(this.item.contentObj.items, '[0].id')) {
this.item.contentObj.items.forEach((item, index) => {
item.id = index + 1;
});
}
this.itemBackup = _.cloneDeep(this.item);
}
ngOnInit() { ngOnInit() {
this.item = {}; this.item = {
contentObj: {}
};
this.init();
// 获取存储的数据 // 获取存储的数据
(<any> window).courseware.getData((data) => { ( window as any).courseware.getData((data) => {
if (data) { if (data) {
this.item = data; this.item = data;
} }
this.init(); this.init();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
this.changeDetectorRef.detectChanges(); this.changeDetectorRef.detectChanges();
...@@ -49,30 +218,10 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -49,30 +218,10 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
init() {
}
/**
* 储存图片数据
* @param e
*/
onImageUploadSuccess(e, key) {
this.item[key] = e.url;
this.save();
}
/**
* 储存音频数据
* @param e
*/
onAudioUploadSuccess(e, key) {
this.item[key] = e.url;
this.save();
}
...@@ -80,7 +229,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -80,7 +229,7 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
* 储存数据 * 储存数据
*/ */
save() { save() {
(<any> window).courseware.setData(this.item, null, this.saveKey); (window as any).courseware.setData(this.item, null, this.saveKey);
this.refresh(); this.refresh();
} }
...@@ -93,5 +242,283 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy { ...@@ -93,5 +242,283 @@ export class FormComponent implements OnInit, OnChanges, OnDestroy {
}, 1); }, 1);
} }
resize(rect) {
if (!this.box) {
return;
}
this.ratio = rect.clientWidth / this.box.nativeElement.clientWidth;
this.box.nativeElement.style.transform = `scale(${this.ratio})`;
}
onEdit(item, event) {
item._inEdit = true;
const dom = event.target;
dom.setAttribute('contenteditable', '');
dom.classList.add('p-edit');
dom.focus();
event.stopPropagation();
event.preventDefault();
}
onBlur(item, dom) {
if (dom.innerText) {
if (item.text !== dom.innerText.trim()) {
item.text = dom.innerText.trim();
this.extraData.changeDirtyCount++;
}
item._inEdit = false;
dom.removeAttribute('contenteditable');
dom.classList.remove('p-edit');
} else {
this.onBtnDelete();
}
}
onSelect(item, event) {
this.curItem = item;
this.curDom = event.target;
this.fontSize = this.curItem.fontSize;
this.fontFamily = this.curItem.fontFamily;
event.stopPropagation();
}
onClickBox() {
this.curItem = null;
}
onChangeFontFamily() {
this.curItem.fontFamily = this.fontFamily;
this.extraData.changeDirtyCount++;
}
onChangeFontSize(delta = 0) {
if (!this.curItem) {
return;
}
this.fontSize = this.fontSize + delta;
this.fontSize = Math.max(this.MIN_FONT_SIZE, this.fontSize);
this.fontSize = Math.min(this.MAX_FONT_SIZE, this.fontSize);
this.curItem.fontSize = this.fontSize;
this.extraData.changeDirtyCount++;
}
onChangeFontColor() {
if (!this.curItem) {
return;
}
this.curItem.color = this.fontColor;
this.extraData.changeDirtyCount++;
}
onChangeColor(color) {
if (this.isShowBGColorPane) {
this.item.contentObj.bgColor = color;
this.extraData.changeDirtyCount++;
} else if (this.isShowFontColorPane) {
this.fontColor = this.curItem.color = color;
this.extraData.changeDirtyCount++;
}
}
absolute2Relative(value) {
return value * this.ratio;
}
relative2Absolute(value) {
return value / this.ratio;
}
onMouseDown(item, event) {
if (item !== this.curItem) {
return;
}
if (this.curItem && !this.curItem._inEdit) {
this.isMovingItem = true;
this.moveOffset = {x: event.x, y: event.y};
}
}
onMouseMove(event) {
if (this.isMovingItem) {
if (this.curItem) {
this.curItem.position.left += this.relative2Absolute(event.x - this.moveOffset.x);
this.curItem.position.top += this.relative2Absolute(event.y - this.moveOffset.y);
this.moveOffset = {x: event.x, y: event.y};
this.extraData.changeDirtyCount++;
}
}
}
onMouseUp(event) {
this.isMovingItem = false;
}
onBtnSave() {
this.save();
this.extraData.changeDirtyCount = 0;
}
onMoveSentence(x, y, event) {
if (this.curItem && !this.curItem._inEdit) {
this.curItem.position.left += x;
this.curItem.position.top += y;
this.extraData.changeDirtyCount++;
event.stopPropagation();
event.preventDefault();
}
}
onBtnMoveToCenter(isHorizontal, isVertical) {
if (!this.curItem) {
return;
}
const {clientWidth, clientHeight} = this.curDom;
const {clientWidth: boxClientWidth, clientHeight: boxClientHeight} = this.box.nativeElement;
if (isHorizontal) {
this.curItem.position.left = (boxClientWidth - clientWidth) / 2;
}
if (isVertical) {
this.curItem.position.top = (boxClientHeight - clientHeight) / 2;
}
this.extraData.changeDirtyCount++;
}
onNewSentence(event) {
const left = this.relative2Absolute(event.layerX);
const top = this.relative2Absolute(event.layerY);
const id = _.get(_.last(this.item.contentObj.items), 'id') || 0;
const item = {
id: id + 1,
color: this.fontColor,
text: 'text',
fontFamily: this.fontFamily || 'Arial',
fontSize: this.fontSize || 6,
_inEdit: false,
position: {left, top}
};
this.item.contentObj.items.push(item);
setTimeout(() => {
item._inEdit = true;
const dom = _.get(this.box.nativeElement.getElementsByClassName('p-selected'), '[0]');
if (dom) {
dom.setAttribute('contenteditable', '');
dom.classList.add('p-edit');
dom.focus();
item.position.top -= (dom.clientHeight / 2);
this.extraData.changeDirtyCount++;
document.execCommand('selectAll', false, null);
}
}, 1);
this.curItem = item;
this.extraData.changeDirtyCount++;
}
onBtnDelete() {
const removedItem = _.remove(this.item.contentObj.items, (i) => {
return i === this.curItem;
});
if (removedItem) {
this.extraData.changeDirtyCount++;
_.remove(this.animations, {id: this.curItem.id});
this.curItem = null;
}
}
onBtnRestore() {
this.extraData.changeDirtyCount = 0;
this.curItem = null;
this.item = _.cloneDeep(this.itemBackup);
this.animations = this.item.contentObj.animations;
}
onBtnShowAnimationPane() {
this.isShowAnimationPane = !this.isShowAnimationPane;
}
onBtnToggleDisplayAnimationOrder() {
this.isShowAnimationOrder = !this.isShowAnimationOrder;
}
onBtnMoveAnimation(value) {
// [value] -1: MoveUp, 1: MoveDown
if (!this.selectedAnimation) {
this.nzMessageService.error(`Please select an animation first`);
return;
}
const index = _.findIndex(this.animations, this.selectedAnimation);
const tmp = this.animations[index + value];
if (tmp) {
this.animations[index + value] = this.animations[index];
this.animations[index] = tmp;
this.extraData.changeDirtyCount++;
} else {
// this.nzMessageService.error(`MoveAnimation Error, index = ${index}, length = ${this.animations.length}`);
}
}
onBtnRemoveAnimation() {
const removed = _.remove(this.animations, {id: this.selectedAnimation.id});
if (removed) {
this.extraData.changeDirtyCount++;
}
}
onClickAnimation(item) {
this.selectedAnimation = item;
this.curAnimationName = item.value;
}
onSetAnimation(item) {
if (!this.curItem) {
this.nzMessageService.warning('Please select a text which you want to set animation');
return;
}
const animation = _.find(this.animations, {id: this.curItem.id});
if (!animation) {
this.animations.push({
id: this.curItem.id,
name: item.name,
value: item.value,
initStyle: {opacity: 0},
beforeAnimation: {opacity: 1},
});
this.extraData.changeDirtyCount++;
} else if (animation.name !== item.name) {
animation.name = item.name;
animation.value = item.value;
this.extraData.changeDirtyCount++;
}
}
// curItem changed
onCurItemChanged() {
if (!this.curItem) {
this.selectedAnimation = null;
this.curAnimationName = null;
return;
}
this.selectedAnimation = _.find(this.animations, {id: this.curItem.id});
if (this.selectedAnimation) {
this.curAnimationName = this.selectedAnimation.value;
}
}
// selectedAnimation changed
onSelectedAnimationChanged() {
if (!this.selectedAnimation) {
return;
}
this.curItem = _.find(this.item.contentObj.items, {id: this.selectedAnimation.id});
}
getIndexOfAnimationByItem(item) {
return _.findIndex(this.animations, {id: item.id});
}
} }
import {Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'brLineBreaker'
})
export class BrLineBreakerPipe implements PipeTransform {
transform(value: any, args?: any): any {
return value ? value.replace(/\n/g, '<br />') : '';
}
}
.game-container {
width: 100%;
height: 100%;
background: #ffffff;
background-size: cover;
}
#canvas {
}
@font-face
{
font-family: 'BRLNSDB';
src: url("../../assets/font/BRLNSDB.TTF") ;
}
<div class="game-container" #wrap> <div class="k-full-fill p-content"
<canvas id="canvas" #canvas></canvas> [style.background-color]="data.contentObj.bgColor"
(click)="playNextAnimation($event)"
>
<div class="k-16-9" appResize (resize)="resize($event)">
<div class="position-absolute p-box" #box
>
<div class="position-absolute p-sentence {{item.animationClass}}" id="i-sentence-{{item.id}}"
*ngFor="let item of data.contentObj.items; index as i;"
#list
appCustomStyle [customStyle]="item">
<span class="text" [innerHTML]="item.text | brLineBreaker" (click)="playOutAnimation($event, item)"></span>
</div>
</div>
</div>
</div> </div>
@import '../style/mixin';
.p-content {
padding: 0vh 2vh;
display: flex;
justify-content: center;
align-items: center;
}
.p-box {
width: 1280px;
height: 720px;
left: 0;
top: 0;
transform-origin: top left;
overflow: hidden;
}
.p-sentence {
@include k-no-select();
text-align: left;
}
...@@ -9,8 +9,7 @@ import {res, resAudio} from './resources'; ...@@ -9,8 +9,7 @@ import {res, resAudio} from './resources';
import {Subject} from 'rxjs'; import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators'; import {debounceTime} from 'rxjs/operators';
import * as _ from 'lodash';;
import TWEEN from '@tweenjs/tween.js';
...@@ -18,660 +17,128 @@ import TWEEN from '@tweenjs/tween.js'; ...@@ -18,660 +17,128 @@ import TWEEN from '@tweenjs/tween.js';
@Component({ @Component({
selector: 'app-play', selector: 'app-play',
templateUrl: './play.component.html', templateUrl: './play.component.html',
styleUrls: ['./play.component.css'] styleUrls: ['./play.component.scss']
}) })
export class PlayComponent implements OnInit, OnDestroy { export class PlayComponent implements OnInit, OnDestroy {
@ViewChild('canvas', {static: true }) canvas: ElementRef; @ViewChild('box')
@ViewChild('wrap', {static: true }) wrap: ElementRef; box: ElementRef;
ratio: number;
saveKey = 'ww_text';
items: any[];
animations: any[];
animationIndex = -1;
// 数据 // 数据
data; data;
ctx;
canvasWidth = 1280; // canvas实际宽度
canvasHeight = 720; // canvas实际高度
canvasBaseW = 1280; // canvas 资源预设宽度
canvasBaseH = 720; // canvas 资源预设高度
mx; // 点击x坐标
my; // 点击y坐标
// 资源
rawImages = new Map(res);
rawAudios = new Map(resAudio);
images = new Map();
animationId: any;
winResizeEventStream = new Subject();
audioObj = {};
renderArr;
mapScale = 1;
canvasLeft;
canvasTop;
saveKey = 'test_0011';
btnLeft;
btnRight;
pic1;
pic2;
canTouch = true;
curPic;
@HostListener('window:resize', ['$event'])
onResize(event) {
this.winResizeEventStream.next();
}
ngOnInit() { ngOnInit() {
this.data = {}; this.data = {
contentObj: {}
};
// 获取数据 // 获取数据
const getData = (<any> window).courseware.getData; const getData = (window as any).courseware.getData;
getData((data) => { getData((data) => {
if (data && typeof data == 'object') { if (data && typeof data === 'object') {
this.data = data; this.data = data;
this.init();
} }
// console.log('data:' , data);
// 初始化 各事件监听
this.initListener();
// 若无数据 则为预览模式 需要填充一些默认数据用来显示
this.initDefaultData();
// 初始化 音频资源 if (window["air"]){
this.initAudio(); window["air"].hideAirClassLoading(this.saveKey, this.data);
// 初始化 图片资源 }
this.initImg();
// 开始预加载资源
this.load();
}, this.saveKey); }, this.saveKey);
} }
ngOnDestroy() { ngOnDestroy() {
window['curCtx'] = null;
window.cancelAnimationFrame(this.animationId);
} }
load() {
// 预加载资源
this.loadResources().then(() => {
window["air"].hideAirClassLoading(this.saveKey, this.data);
this.init();
this.update();
});
}
init() { init() {
this.initCtx(); this.items = _.get(this.data.contentObj, 'items');
this.initData(); _.forEach(this.items, item => item.animationClass = null);
this.initView();
}
initCtx() {
this.canvasWidth = this.wrap.nativeElement.clientWidth;
this.canvasHeight = this.wrap.nativeElement.clientHeight;
this.canvas.nativeElement.width = this.wrap.nativeElement.clientWidth;
this.canvas.nativeElement.height = this.wrap.nativeElement.clientHeight;
this.ctx = this.canvas.nativeElement.getContext('2d');
this.canvas.nativeElement.width = this.canvasWidth;
this.canvas.nativeElement.height = this.canvasHeight;
window['curCtx'] = this.ctx;
}
this.animations = _.get(this.data.contentObj, 'animations');
if (this.items && this.animations) {
updateItem(item) { _.forEach(this.animations, (animation) => {
if (item) { const item = _.find(this.items, {id: animation.id});
item.update(); if (item) {
_.assign(item, animation.initStyle);
}
});
} }
} }
resize(rect) {
updateArr(arr) { if (!this.box) {
if (!arr) {
return; return;
} }
for (let i = 0; i < arr.length; i++) { this.ratio = rect.clientWidth / this.box.nativeElement.clientWidth;
arr[i].update(this); this.box.nativeElement.style.transform = `scale(${this.ratio})`;
} }
} playOutAnimation(evt, item) {
evt.stopPropagation();
console.log('out out out ');
// const idx = this.animations.findIndex((el) => {
// return item === el;
// });
// const animation = _.get(this.animations, `[${this.animationIndex}]`);
const tEl = evt.target;
initListener() { if (tEl.tagName === 'SPAN' && tEl.className === 'text') {
// const idx = _.findIndex(this.box.nativeElement.children, el => {
this.winResizeEventStream // return tEl.parentNode === el;
.pipe(debounceTime(500)) // });
.subscribe(data => { if (!item.animationClass) {
this.renderAfterResize();
});
// ---------------------------------------------
const setParentOffset = () => {
const rect = this.canvas.nativeElement.getBoundingClientRect();
this.canvasLeft = rect.left;
this.canvasTop = rect.top;
};
const setMxMyByTouch = (event) => {
if (event.touches.length <= 0) {
return; return;
} }
if (this.canvasLeft == null) { // const anim = _.get(this.animations, `[${idx}]`);
setParentOffset(); const css = item.animationClass.split(' ')[1];
} tEl.parentNode.classList.remove(css);
this.mx = event.touches[0].pageX - this.canvasLeft; let outCss = '';
this.my = event.touches[0].pageY - this.canvasTop; if (css === 'fadeInRight') {
}; outCss = 'fadeOutLeft';
} else if (css === 'fadeInDown') {
const setMxMyByMouse = (event) => { outCss = 'fadeOutDown';
this.mx = event.offsetX; } else if (css === 'zoomInUp') {
this.my = event.offsetY; outCss = 'zoomOutUp';
}; } else if (css === 'rotateIn') {
// --------------------------------------------- outCss = 'rotateOut';
} else if (css === 'rollIn') {
outCss = 'rollOut';
let firstTouch = true;
const touchDownFunc = (e) => {
if (firstTouch) {
firstTouch = false;
removeMouseListener();
}
setMxMyByTouch(e);
this.mapDown(e);
};
const touchMoveFunc = (e) => {
setMxMyByTouch(e);
this.mapMove(e);
};
const touchUpFunc = (e) => {
setMxMyByTouch(e);
this.mapUp(e);
};
const mouseDownFunc = (e) => {
if (firstTouch) {
firstTouch = false;
removeTouchListener();
}
setMxMyByMouse(e);
this.mapDown(e);
};
const mouseMoveFunc = (e) => {
setMxMyByMouse(e);
this.mapMove(e);
};
const mouseUpFunc = (e) => {
setMxMyByMouse(e);
this.mapUp(e);
};
const element = this.canvas.nativeElement;
const addTouchListener = () => {
element.addEventListener('touchstart', touchDownFunc);
element.addEventListener('touchmove', touchMoveFunc);
element.addEventListener('touchend', touchUpFunc);
element.addEventListener('touchcancel', touchUpFunc);
};
const removeTouchListener = () => {
element.removeEventListener('touchstart', touchDownFunc);
element.removeEventListener('touchmove', touchMoveFunc);
element.removeEventListener('touchend', touchUpFunc);
element.removeEventListener('touchcancel', touchUpFunc);
};
const addMouseListener = () => {
element.addEventListener('mousedown', mouseDownFunc);
element.addEventListener('mousemove', mouseMoveFunc);
element.addEventListener('mouseup', mouseUpFunc);
};
const removeMouseListener = () => {
element.removeEventListener('mousedown', mouseDownFunc);
element.removeEventListener('mousemove', mouseMoveFunc);
element.removeEventListener('mouseup', mouseUpFunc);
};
addMouseListener();
addTouchListener();
}
playAudio(key, now = false, callback = null) {
const audio = this.audioObj[key];
if (audio) {
if (now) {
audio.pause();
audio.currentTime = 0;
}
if (callback) {
audio.onended = () => {
callback();
};
} }
audio.play();
}
}
loadResources() {
const pr = [];
this.rawImages.forEach((value, key) => {// 预加载图片
const p = this.preload(value)
.then(img => {
this.images.set(key, img);
})
.catch(err => console.log(err));
pr.push(p);
});
this.rawAudios.forEach((value, key) => {// 预加载音频
const a = this.preloadAudio(value)
.then(() => {
// this.images.set(key, img);
})
.catch(err => console.log(err));
pr.push(a);
});
return Promise.all(pr);
}
preload(url) {
return new Promise((resolve, reject) => {
const img = new Image();
// img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = reject;
img.src = url;
});
}
preloadAudio(url) {
return new Promise((resolve, reject) => {
const audio = new Audio();
audio.oncanplay = (a) => {
resolve();
};
audio.onerror = () => {
reject();
};
audio.src = url;
audio.load();
});
}
renderAfterResize() { tEl.parentNode.classList.add(outCss);
this.canvasWidth = this.wrap.nativeElement.clientWidth;
this.canvasHeight = this.wrap.nativeElement.clientHeight;
this.init();
}
checkClickTarget(target) {
const rect = target.getBoundingBox();
if (this.checkPointInRect(this.mx, this.my, rect)) {
return true;
} }
return false;
} }
playNextAnimation(evt) {
getWorlRect(target) {
let rect = target.getBoundingBox(); this.animationIndex++;
// this.animationIndex = this.animationIndex % this.animations.length;
if (target.parent) { const animation = _.get(this.animations, `[${this.animationIndex}]`);
if (animation) {
const pRect = this.getWorlRect(target.parent); const item = _.find(this.items, {id: animation.id});
rect.x += pRect.x; if (item) {
rect.y += pRect.y; // dom已经存在了, 需要直接操作, 用ng的方式需要绕一下
} const dom = _.find(this.box.nativeElement.children, {id: `i-sentence-${animation.id}`});
return rect; if (dom) {
} _.assign(dom.style, animation.beforeAnimation);
item.animationClass = animation.value;
checkPointInRect(x, y, rect) { // console.log(animation.beforeAnimation, animation.value);
if (x >= rect.x && x <= rect.x + rect.width) { }
if (y >= rect.y && y <= rect.y + rect.height) {
return true;
} }
} }
return false;
}
addUrlToAudioObj(key, url = null, vlomue = 1, loop = false, callback = null) {
const audioObj = this.audioObj;
if (url == null) {
url = key;
}
this.rawAudios.set(key, url);
const audio = new Audio();
audio.src = url;
audio.load();
audio.loop = loop;
audio.volume = vlomue;
audioObj[key] = audio;
if (callback) {
audio.onended = () => {
callback();
};
}
}
addUrlToImages(url) {
this.rawImages.set(url, url);
}
// ======================================================编写区域==========================================================================
/**
* 添加默认数据 便于无数据时的展示
*/
initDefaultData() {
if (!this.data.pic_url) {
this.data.pic_url = 'assets/play/default/pic.jpg';
this.data.pic_url_2 = 'assets/play/default/pic.jpg';
}
}
/**
* 添加预加载图片
*/
initImg() {
this.addUrlToImages(this.data.pic_url);
this.addUrlToImages(this.data.pic_url_2);
}
/**
* 添加预加载音频
*/
initAudio() {
// 音频资源
this.addUrlToAudioObj(this.data.audio_url);
this.addUrlToAudioObj(this.data.audio_url_2);
// 音效
this.addUrlToAudioObj('click', this.rawAudios.get('click'), 0.3);
}
/**
* 初始化数据
*/
initData() {
const sx = this.canvasWidth / this.canvasBaseW;
const sy = this.canvasHeight / this.canvasBaseH;
const s = Math.min(sx, sy);
this.mapScale = s;
// this.mapScale = sx;
// this.mapScale = sy;
this.renderArr = [];
}
/**
* 初始化试图
*/
initView() {
this.initPic();
this.initBottomPart();
}
initBottomPart() {
const btnLeft = new MySprite();
btnLeft.init(this.images.get('btn_left'));
btnLeft.x = this.canvasWidth - 150 * this.mapScale;
btnLeft.y = this.canvasHeight - 100 * this.mapScale;
btnLeft.setScaleXY(this.mapScale);
this.renderArr.push(btnLeft);
this.btnLeft = btnLeft;
const btnRight = new MySprite();
btnRight.init(this.images.get('btn_right'));
btnRight.x = this.canvasWidth - 50 * this.mapScale;
btnRight.y = this.canvasHeight - 100 * this.mapScale;
btnRight.setScaleXY(this.mapScale);
this.renderArr.push(btnRight);
this.btnRight = btnRight;
}
initPic() {
const maxW = this.canvasWidth * 0.7;
const pic1 = new MySprite();
pic1.init(this.images.get(this.data.pic_url));
pic1.x = this.canvasWidth / 2;
pic1.y = this.canvasHeight / 2;
pic1.setScaleXY(maxW / pic1.width);
this.renderArr.push(pic1);
this.pic1 = pic1;
const label1 = new Label();
label1.text = this.data.text;
label1.textAlign = 'center';
label1.fontSize = 50;
label1.fontName = 'BRLNSDB';
label1.fontColor = '#ffffff';
pic1.addChild(label1);
const pic2 = new MySprite();
pic2.init(this.images.get(this.data.pic_url_2));
pic2.x = this.canvasWidth / 2 + this.canvasWidth;
pic2.y = this.canvasHeight / 2;
pic2.setScaleXY(maxW / pic2.width);
this.renderArr.push(pic2);
this.pic2 = pic2;
this.curPic = pic1;
}
btnLeftClicked() {
this.lastPage();
}
btnRightClicked() {
this.nextPage();
}
lastPage() {
if (this.curPic == this.pic1) {
return;
}
this.canTouch = false;
const moveLen = this.canvasWidth;
tweenChange(this.pic1, {x: this.pic1.x + moveLen}, 1);
tweenChange(this.pic2, {x: this.pic2.x + moveLen}, 1, () => {
this.canTouch = true;
this.curPic = this.pic1;
});
}
nextPage() {
if (this.curPic == this.pic2) {
return;
}
this.canTouch = false;
const moveLen = this.canvasWidth;
tweenChange(this.pic1, {x: this.pic1.x - moveLen}, 1);
tweenChange(this.pic2, {x: this.pic2.x - moveLen}, 1, () => {
this.canTouch = true;
this.curPic = this.pic2;
});
}
pic1Clicked() {
this.playAudio(this.data.audio_url);
}
pic2Clicked() {
this.playAudio(this.data.audio_url_2);
}
mapDown(event) {
if (!this.canTouch) {
return;
}
if ( this.checkClickTarget(this.btnLeft) ) {
this.btnLeftClicked();
return;
}
if ( this.checkClickTarget(this.btnRight) ) {
this.btnRightClicked();
return;
}
if ( this.checkClickTarget(this.pic1) ) {
this.pic1Clicked();
return;
}
if ( this.checkClickTarget(this.pic2) ) {
this.pic2Clicked();
return;
}
}
mapMove(event) {
}
mapUp(event) {
}
update() {
// ----------------------------------------------------------
this.animationId = window.requestAnimationFrame(this.update.bind(this));
// 清除画布内容
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// tween 更新动画
TWEEN.update();
// ----------------------------------------------------------
this.updateArr(this.renderArr);
} }
......
@mixin hide-overflow-text {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
@mixin k-no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
@mixin k-img-bg {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
/* You can add global styles to this file, and also import other style files */
/* You can add global styles to this file, and also import other style files */
/* You can add global styles to this file, and also import other style files */
/* You can add global styles to this file, and also import other style files */
/* You can add global styles to this file, and also import other style files */
html, body {
width: 100%;
height: 100%;
font-size: 16px;
}
body {
background-color: #f0f2f5 !important;
}
* {
outline: none;
}
@font-face {
font-family: ChalkboardSE-Regular;
src: url('/assets/font/ChalkboardSE-Regular.woff') format('WOFF');
}
.k-16-9 {
position: relative;
width: 100%;
height: fit-content;
&:before {
content: "";
display: block;
position: relative;
width: 100%;
height: 0;
padding-bottom: 56.25%;
}
}
.k-full-fill {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.k-flex-center {
display: flex;
align-items: center;
justify-content: center;
}
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