블록코딩 콘텐츠 제작을 위한 Blockly 개발 시작하기
Blockly, pixi.js 사용해서 블록코딩 그림판 만들기 - 1
지난 포스팅에서 블록코딩 그림판 제작을 위한 그림판 프로그램을 만들어 보았습니다. 이번 시간에도 이어서 계속 진행해 보겠습니다,
이전에 만들어진 그림판 프로그램은 그리기 명령 함수 호출 시 해당하는 그림이 바로 그려졌습니다. 명령 호출 순서에 따라 그려지는 모습을 확인할 수 있게 그리기 애니메이션 효과를 만들어 보겠습니다.
그리기 명령 함수 호출 시 그림을 바로 그리지 않고 배열에 그리기 명령을 순서대로 저장해 놓았다가 실행 함수가 호출 되었을 때 하나하나 수행 되도록 하겠습니다. 이를 위해 명령을 저장할 commands
배열과 실행 함수인 run()
그리고 애니메이션 효과를 업데이트 하기 위한 업데이트 루프도 만들어 보도록 하겠습니다.
PaintApp 클레스에 다음 코드들을 추가합니다.
//paint/paintApp.js
constructor(parentEl) {
...
//그리기 명령을 순서대로 저장할 배열
this.commands = [];
//그리기 애니메이션이 진행 중인 명령
this.currentCommand = null;
//그리기가 실행되고 있는지 여부
this.isRun = false;
//pixi.js 에서 업데이트 루프를 만들기 위한 방법
//t: 초당 60프레임 기준으로 이전 프레임과의 시간 격차를 나타냄. 1이면 60분의 1초임을 나타냄
this.app.ticker.add((t)=>{
//실제 흐른 시간 단위로 변환
const deltaTime = t / 60;
this.update(deltaTime);
});
}
//업데이트 루프
update(dt) {
if(!this.isRun) {
return;
}
}
run() {
if(this.isRun || !this.commands.length) {
return;
}
this.isRun = true;
}
기존의 그리기 함수들도 다음과 같이 변경합니다.
drawLine(x1, y1, x2, y2, color = 0x000000, weight = 2) {
this.commands.push({
type: 'line',
x1, y1, x2, y2,
color, weight,
})
}
drawRect(left, top, width, height, color = 0x000000, weight = 2) {
this.commands.push({
type: 'rect',
left, top, width, height,
color, weight
});
}
drawCircle(x, y, radius, color = 0x000000, weight = 2) {
this.commands.push({
type: 'circlr',
x, y, radius,
color, weight
});
}
drawRectFill(left, top, width, height, color = 0x000000) {
this.commands.push({
type: 'rectFill',
left, top, width, height,
color
});
}
drawCircleFill(x, y, radius, color = 0x000000) {
this.commands.push({
type: 'circleFill',
x, y, radius,
color
});
}
drawPolygon(points, color = 0x000000, weight = 2) {
if(!points || points.length <= 2) return;
this.commands.push({
type: 'polygon',
points,
color, weight,
});
}
drawPolygonFill(points, color = 0x000000) {
if(!points || points.length <= 2) return;
this.commands.push({
type: 'polygonFill',
points,
color,
});
}
그리기 명령이 실행 중일 때 커맨드 배열에서 커맨드를 하나씩 꺼내서 수행 하도록 update함수를 수정합니다.
update(dt) {
if(!this.isRun) {
return;
}
if(!this.currentCommand) {
if(this.commands.length > 0) {
const graphics = new PIXI.Gr
this.container.addChild(grap
this.currentCommand = {
command: this.commands.s
graphics,
t: 0,
}
}
else {
this.isRun = false;
return;
}
}
const {t, graphics, command} = this.currentCommand;
const {type} = command;
//이전에 그려진 그림을 지움
graphics.clear();
const progress = Math.min(t, 1);
switch(type) {
case 'line': {
break;
}
case 'rect': {
break;
}
case 'circle': {
break;
}
case 'polygon': {
break;
}
case 'rectFill': {
break;
}
case 'circleFill': {
break;
}
case 'polygonFill': {
break;
}
}
if(progress >= 1) {
this.currentCommand = null;
}
else {
this.currentCommand.t += dt;
}
}
선 그리기 부터 시작점부터 끝점까기 서서히 그려지도록 애니메이션 효과를 만들어 보겠습니다. progress
가 0 ~ 1 로 증가함에 따라 선도 동일하게 끝점까지 그려집니다.
case 'line': {
const {x1, y1, x2, y2, color, weight} = this.currentCommand.command;
graphics.lineStyle(weight, color, 1);
graphics.moveTo(x1, y1);
graphics.lineTo(
x1 + (x2 - x1) * progress,
y1 + (y2 - y1) * progress
);
break;
}
사각형 그리기는 기존에 사용하던 drawRect
대신에 moveTo
와 lineTo
함수를 사용해서 한 변씩 그려지게 하겠습니다. progress
를 한 변에 4분의 1 인 0.25 만큼 나눠서 그려지게 합니다. 0~0.25 구간에서는 윗 변이 그려지고 0.25~0.5 구간에는 오른쪽 변이 0.5~0.75에서는 아랫쪽 변, 0.75~1 구간에서는 왼쪽변이 그려집니다.
case 'rect':
case 'rectFill': {
const {left, top, width, height, color, weight} = this.currentCommand.command;
if(type === 'rectFill') {
graphics.beginFill(color);
}
else {
graphics.lineStyle(weight, color, 1);
}
graphics.moveTo(left, top);
const right = left + width;
const bottom = top + height;
const t1 = progress > 0.25 ? 1 : progress / 0.25;
const t2 = progress > 0.5 ? 1 : (progress - 0.25) / 0.25;
const t3 = progress > 0.75 ? 1 : (progress - 0.5) / 0.25;
const t4 = progress > 1 ? 1 : (progress - 0.75) / 0.25;
graphics.lineTo(left + width * t1, top);
if(progress > 0.25) graphics.lineTo(right, top + height * t2);
if(progress > 0.5) graphics.lineTo(right - width * t3, bottom);
if(progress > 0.75) graphics.lineTo(left, bottom - height * t4);
break;
}
원 그리기도 기존에 사용하던 drawCircle
대신 호를 그려주는 arc
함수를 사용합니다.
case 'circle':
case 'circleFill': {
const {x, y, radius, color, weight} = this.currentCommand.command;
if(type === 'circleFill') {
graphics.beginFill(color);
}
else {
graphics.lineStyle(weight, color, 1);
}
//0도 부터 360 * progress 도 까지 서서시 그림
graphics.arc(x, y, radius, 0, Math.PI * 2 * progress);
break;
}
다각형 그리기는 사각형을 그리는 형태와 비슷하게 반복문을 사용해서 구현해 줍니다.
case 'polygon':
case 'polygonFill': {
const {points, color, weight} = this.currentCommand.command;
if(type === 'polygonFill') {
graphics.beginFill(color);
}
else {
graphics.lineStyle(weight, color, 1);
}
const startPoint = points[0];
graphics.moveTo(startPoint.x, startPoint.y);
//한 변에 사용할 퍼센트
const step = 1 / points.length;
for (let i = 1; i <= points.length; i++) {
const crtT = step * ( i - 1 );
if(progress < crtT) {
break;
}
const idx = i % points.length;
const prevIdx = (i - 1) % points.length;
const prevPoint = points[prevIdx];
const nextPoint = points[idx];
const t1 = (progress - crtT) / step;
const t2 = Math.min(t1, 1);
graphics.lineTo(
prevPoint.x + (nextPoint.x - prevPoint.x) * t2,
prevPoint.y + (nextPoint.y - prevPoint.y) * t2);
}
break;
paintTest.js 파일에서 run
함수를 추가적으로 호출하여 테스트 해보겠습니다.
...
paintApp.run();
블록코딩에서 사용할 그림판 프로그램 제작이 거의 마무리 되었습니다. 지금까지는 그리기 코드를 수정하고 페이지 새로고침을 통해 그림을 갱신하였습니다. 하지만 블록코딩에서 사용할때는 페이지 새로고침 없이 그림판을 클리어하고 다시 그릴 수 있어야 합니다. 이를 위해 PaintApp 클레스에 clear 함수를 추가합니다.
clear() {
this.commands = [];
this.currentCommand = null;
//콘테이너에 추가되어 있는 PIXI.Graphics 객체들을 모두 제거함.
this.container.removeChildren();
}
이상으로 그림판 프로그램 제작은 마치겠습니다. 다음 포스팅 부터는 Blockly의 워크스페이스에 그림판을 제어할 수 있는 커스텀 블록을 추가하는 방법과 변환된 코드를 적용하는 방법을 제작해보겠습니다.