트리 이미지 생성, 세 번째 부분.분형에서 진짜 나무로
44900 단어 typescriptoopdddarchitecture
두 번째 부분에서 우리는 응용 프로그램 구조를 설계하고 환경과 의존 주입을 설정했다.마지막으로 트리의 문자열 표현을 생성할 수 있는 L-Systems 모듈을 만들었습니다.
에서 캔버스 요소에 액세스하기 위해 형상 모듈과 DOM 어댑터를 생성했습니다.마지막으로, 우리는 화면에 첫 번째 그림을 보여 주었다.
마지막 글에서 우리는 L 시스템 역할에 대한 번역기를 만들 것이다.그 밖에 우리는 피타고라스 나무를 생성하고 랜덤성을 첨가하여 진정한 나무처럼 보일 것이다.
마지막으로 다음과 같이 트리 이미지를 생성하는 응용 프로그램을 만듭니다.
제작 번역
해석기 모듈은 L 시스템의 문자열 표현을 받아들여 하나의 그림 명령으로 번역합니다.API를 설계하고 종속성을 정의합니다.
이 모듈은 기하학적 모듈에 의존하고
SystemInterpreter
인터페이스를 제공한다.// src/interpreter/types.ts
export interface SystemInterpreter {
translate(expression: Expression): List<Line>;
}
이 인터페이스를 실현해 봅시다.// src/interpreter/implementation.ts
import { AppSettings } from "../settings";
import { StartSelector } from "../geometry/location";
import { ShapeBuilder } from "../geometry/shape";
import { Stack } from "./stack/types";
import { SystemInterpreter } from "./types";
export class SystemToGeometryMapper implements SystemInterpreter {
// We will change these fields while translating commands:
private currentLocation: Point = { x: 0, y: 0 };
private currentAngle: DegreesAmount = 0;
// These fields keep characters of an initial expression
// and a list of corresponding commands:
private systemTokens: List<Character> = [];
private drawInstructions: List<Line> = [];
// Define dependencies:
constructor(
private shapeBuilder: ShapeBuilder,
private startSelector: StartSelector,
private stack: Stack<TreeJoint>,
private settings: AppSettings,
) {}
// Implement public methods:
public translate(expression: Expression): List<Line> {
this.currentLocation = { ...this.startSelector.selectStart() };
this.systemTokens = expression.split("");
this.systemTokens.forEach(this.translateToken);
return this.drawInstructions;
}
// …
}
translate
방법에서는 L 시스템 표현식을 사용하여 단일 문자로 분할했습니다.그리고 우리는 translateToken
방법으로 모든 문자를 처리하고, 잠시 후에 이 방법을 작성할 것이다.따라서 번역된 명령이 모두 포함된
drawInstructions
목록을 되돌려줍니다.You may notice the
Stack<TreeJoint>
in the dependencies. This is literal stack structure implementation. You can find its code on GitHub.
그리고 private
translateToken
방법을 만듭니다.// src/interpreter/implementation.ts
export class SystemToGeometryMapper implements SystemInterpreter {
// …
private translateToken = (token: Character): void => {
switch (token) {
// If the character is 0 or 1
// we draw a line from the current position
// with a current angle:
case "0":
case "1": {
const line = this.shapeBuilder.createLine(
this.currentLocation,
this.settings.stemLength,
this.currentAngle,
);
this.drawInstructions.push(line);
this.currentLocation = { ...line.end };
break;
}
// If the character is an opening bracket we turn left
// and push the current position and angle in the stack:
case "[": {
this.currentAngle -= this.settings.jointAngle;
this.stack.push({
location: { ...this.currentLocation },
rotation: this.currentAngle,
stemWidth: this.settings.stemLength,
});
break;
}
// If the character is the closing bracket
// we pop the last position and the angle from the stack
// and turn right from there:
case "]": {
const lastJoint = this.stack.pop();
this.currentLocation = { ...lastJoint.location };
this.currentAngle = lastJoint.rotation + 2 * this.settings.jointAngle;
break;
}
}
};
}
이제 영패에 이 방법을 사용할 때, 그것은 새로운 선을 그리거나, 어느 쪽으로 돌아갈지 결정한다.창고는 우리가 마지막 지점으로 돌아가는 것을 돕는다.이제 설정을 업데이트하고 이 메서드를 실행하여 다음 사항을 살펴보겠습니다.
// src/settings/index.ts
export const settings: AppSettings = {
canvasSize: {
width: 800,
height: 600,
},
// Using 5 iterations
// with Pythagoras tree rules:
iterations: 5,
initiator: "0",
rules: {
"1": "11",
"0": "1[0]0",
},
// Stem length is 10 pixels;
// turn 45 degrees each time:
stemLength: 10,
jointAngle: 45,
};
애플리케이션 포털 업데이트:// src/index.ts
const builder = container.get<SystemBuilder>();
const drawer = container.get<Drawer>();
const interpreter = container.get<SystemInterpreter>();
const settings = container.get<AppSettings>();
const system = builder.build(settings);
const lines = interpreter.translate(system);
lines.forEach((line) => drawer.drawLine(line));
이러한 설정을 통해 우리는 규범화된 피타고라스 나무를 얻었다.우리는 각도 게임을 해서 우리가 어떤 데이터를 얻을 수 있는지 볼 수 있다😃
90도에서 우리는 안테나를 얻었다.
15도의 온도에서 우리는 풀 한 조각을 얻었다.
화씨 115도, 우리는 얻는다...음...
쿨!우리는 이미 이 나무의 기본 지식을 가지고 있다.하지만 우리가 그것을 더욱 진실하게 만들기 전에 입구점을 정리해야 한다.
입구점 정리
지금 입구가 좀 더러워요.
// src/index.ts
const builder = container.get<SystemBuilder>();
const drawer = container.get<Drawer>();
const interpreter = container.get<SystemInterpreter>();
const settings = container.get<AppSettings>();
const system = builder.build(settings);
const lines = interpreter.translate(system);
lines.forEach((line) => drawer.drawLine(line));
우리는 용기에서 너무 많은 서비스를 받아서 모든 조작을 수동으로 초기화했다.응용 프로그램 시작을 담당하는 개별 객체에 이러한 모든 것을 숨깁니다.// src/app/types.ts
export interface Application {
start(): void;
}
이제 모든 코드를 start
메서드 뒤에 숨깁니다.// src/app/implementation.ts
export class App implements Application {
constructor(
private builder: SystemBuilder,
private drawer: Drawer,
private interpreter: SystemInterpreter,
private settings: AppSettings,
) {}
start(): void {
const system = this.builder.build(this.settings);
const lines = this.interpreter.translate(system);
lines.forEach((line) => this.drawer.drawLine(line));
}
}
...등록:// src/app/composition.ts
import { container } from "../composition";
import { App } from "./implementation";
import { Application } from "./types";
container.registerSingleton<Application, App>();
이제 입구가 더 깨끗해졌어요.// src/index.ts
import { container } from "./composition";
import { Application } from "./app/types";
const app = container.get<Application>();
app.start();
나무를 더 진실하게
지금 우리의 나무는 너무 엄격하고 수학적이다.보다 사실적으로 보이기 위해서는 다음과 같은 몇 가지 임의성과 역동성을 추가해야 합니다.
"2"
를 추가했다.현재 나무 모양의 분지는 매번 교체될 때마다 두 배로 줄어들고 새로운 상수는 이 과정을 늦춘다.우리는 또 공리를 좀 더 길게 해서 나무 줄기를 더욱 길게 할 것이다.마지막으로 우리는 교체 횟수를 12회로 늘릴 것이다.
// src/settings/index.ts
export const settings: AppSettings = {
// …
iterations: 12,
initiator: "22220",
rules: {
"1": "21",
"0": "1[20]20",
},
leafWidth: 4,
stemWidth: 16,
// …
};
이제 해석기 코드를 변경하겠습니다.export class SystemToGeometryMapper implements SystemInterpreter {
private currentLocation: Point = { x: 0, y: 0 };
private currentAngle: DegreesAmount = 0;
// We will also change the stem width:
private currentWidth: PixelsAmount = 0;
private systemTokens: List<Character> = [];
private drawInstructions: List<Instruction> = [];
constructor(
private shapeBuilder: ShapeBuilder,
private startSelector: StartSelector,
private stack: Stack<TreeJoint>,
private settings: AppSettings,
// Here, we're going to need a random source.
// In our case, it is a wrapper over `Math.random`
// with a bit more convenient API.
// You can find its source on GitHub as well.
private random: RandomSource,
) {}
// …
}
그리고 잎사귀 (("0"
문자) 를 처리하면 녹색을 무작위로 선택합니다.private translateToken = (token: Character): void => {
switch (token) {
case "0": {
const line = this.createLine();
this.currentLocation = { ...line.end };
this.drawInstructions.push({
line,
color: this.selectLeafColor(), // Adding the leaf color
width: this.settings.leafWidth, // and width.
});
break;
}
// …
}
}
그리고 나서 우리는 때때로 새로운 지점을 뛰어넘는다.이로 인해 브랜치 위치가 더욱 혼란스러워집니다.private translateToken = (token: Character): void => {
switch (token) {
// …
case "1":
case "2": {
// Draw a new branch only in 60% of cases:
if (this.shouldSkip()) return;
const line = this.createLine();
this.drawInstructions.push({ line, width: this.currentWidth });
this.currentLocation = { ...line.end };
break;
}
// …
}
};
회전 시 각도 값에 임의 편차를 추가합니다.private translateToken = (token: Character): void => {
switch (token) {
// …
case "[": {
// Making the width smaller:
this.currentWidth *= 0.75;
// Adding a random angle deviation:
this.currentAngle -=
this.settings.jointAngle + this.randomAngleDeviation();
// Remember the branching position,
// angle, and current branch width:
this.stack.push({
location: { ...this.currentLocation },
rotation: this.currentAngle,
stemWidth: this.currentWidth,
});
break;
}
case "]": {
// Getting the last branching position:
const lastJoint = this.stack.pop();
// Using its position, angle, and width as current:
this.currentWidth = lastJoint.stemWidth;
this.currentLocation = { ...lastJoint.location };
this.currentAngle =
lastJoint.rotation +
2 * this.settings.jointAngle +
this.randomAngleDeviation();
break;
}
}
};
또한 부족한 모든 사유 방법도 추가했습니다.export class SystemToGeometryMapper implements SystemInterpreter {
// …
private createLine = (): Line => {
return this.shapeBuilder.createLine(
this.currentLocation,
this.settings.stemLength,
this.currentAngle,
);
};
// Draw branches only 60% of the time:
private shouldSkip = (): boolean => {
return this.random.getValue() > 0.4;
};
// Random deviation will be from -5 to 5 degrees:
private randomAngleDeviation = (): Angle => {
return this.random.getBetweenInclusive(-5, 5);
};
// Green color will be chosen among 3 different colors:
private selectLeafColor = (): Color => {
const randomColor = this.random.getBetweenInclusive(0, 2);
return leafColors[randomColor];
};
}
마지막으로 애플리케이션을 실행하고 결과를 살펴보겠습니다.우리는 진정한 나무 한 그루가 있다!🌳
변화는 현지의 것이다
중요한 것은 최근에 변경한 사항이
Interpreter
모듈의 제한을 받았다는 것이다.비록 이미지에 커다란 변화가 생겼지만, 우리는 해석기의 실현만 바꾸었다.다른 모든 모듈은 그대로 유지됩니다.더 중요한 것은 인터페이스도 마찬가지다.우리는
SystemInterpreter
와 ShapeBuilder
를 바꿀 필요가 없다.우리는 심지어 완전히 바꾸어 실현할 수 있다!인터페이스가 같기만 하면 프로그램은 아무런 추가 변경도 필요 없이 작업을 할 수 있다.
결실
이제 전체 시스템을 살펴보겠습니다.
모듈은 인터페이스를 통해 통신한다.
그것은 테스트에 편리하다.각 모듈은 개별적으로 테스트할 수 있습니다.의존항은 mock objects로 바꾸어 같은 인터페이스를 실현할 수 있다.
인터페이스는 세부 사항을 테스트할 필요가 없기 때문에 우리에게 무엇을 테스트해야 하는지 알려준다.인터페이스는 공용 API를 보여주고 테스트할 내용을 보여줍니다.
또 다른 장점은 서로 다른 가방에서 모듈을 조합하는 것이다.“DDD, Hexagonal, Onion, Clean, CQRS, …How I put it all together”에서 이에 대해 상세하게 설명하였다.
예를 들어, 응용 프로그램 계층별로 패키지를 분할할 수 있습니다.
의존항은 항상 영역을 가리킨다.이것은 역층을 완전히 독립시키기 때문에 우리는 많은 다른 응용 프로그램 사이에서 코드를 공유할 수 있다.
내부 핵과 인프라 공유에 대한 방주
인프라는 일반적으로 데이터베이스, 검색엔진 및 기타 외부 구동 서비스를 연결하는 코드이다.
Google 응용 프로그램에는 결과를 어떤 방식으로도 저장할 필요가 없기 때문에 인프라가 없습니다.
만약 우리가 그것을 가지고 있다면 인프라 시설 모듈은 매우 비슷할 것이다.
Reference
이 문제에 관하여(트리 이미지 생성, 세 번째 부분.분형에서 진짜 나무로), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/bespoyasov/generating-trees-images-part-3-from-fractal-to-a-real-tree-26nb텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)