diff --git a/src/app/components/Octicon.ts b/src/app/components/Octicon.ts
index 63965eeb..fdd9486a 100644
--- a/src/app/components/Octicon.ts
+++ b/src/app/components/Octicon.ts
@@ -1,4 +1,5 @@
export const Octicon = {
+ arrow_both: '',
arrow_left: '',
arrow_right: '',
chevron_down: '',
@@ -7,6 +8,7 @@ export const Octicon = {
code: '',
dash: '',
download: '',
+ gear: '',
globe: '',
history: '',
info: '',
@@ -19,6 +21,8 @@ export const Octicon = {
play: '',
plus: '',
plus_circle: '',
+ square: '',
+ square_fill: '',
sun: '',
tag: '',
trashcan: '',
diff --git a/src/app/components/panels/PreviewPanel.ts b/src/app/components/panels/PreviewPanel.ts
index 683c4235..86dd0b09 100644
--- a/src/app/components/panels/PreviewPanel.ts
+++ b/src/app/components/panels/PreviewPanel.ts
@@ -7,20 +7,30 @@ import { Octicon } from '../Octicon';
export const PreviewPanel = (view: View, model: DataModel) => {
const panel = view.register(el => {
- const canvasEl = el.querySelector('canvas')!
- const controlsEl = el.querySelector('.panel-controls')!
+ const canvas = el.querySelector('canvas')!
const redraw = () => {
const preview = App.preview.get()
if (preview && preview.path && preview.path.withModel(model).get()) {
- const ctx = canvasEl.getContext('2d')!
- const img = ctx.createImageData(200, 100)
+ const ctx = canvas.getContext('2d')!
const newState = preview.path.withModel(model).get()
preview.state = JSON.parse(JSON.stringify(newState))
+ const [width, height] = preview.getSize()
+ canvas.width = width
+ canvas.height = height
+ const img = ctx.createImageData(width, height)
preview.draw(model, img)
ctx.putImageData(img, 0, 0)
} else {
App.preview.set(null)
}
+
+ view.mount(el.querySelector('.panel-controls')!, `
+ ${App.preview.get()?.menu(view, redraw) ?? ''}
+
+ ${Octicon.x}
+
`, false)
}
model.addListener({
invalidated: redraw
@@ -44,23 +54,22 @@ export const PreviewPanel = (view: View, model: DataModel) => {
;(el as HTMLCanvasElement).addEventListener('mousemove', evt => {
if (dragStart === undefined) return
if (App.preview.get()?.onDrag) {
- App.preview.get()?.onDrag(dragStart[0], dragStart[1], evt.offsetX, evt.offsetY)
- redraw()
+ const [width, height] = App.preview.get()!.getSize()
+ const dx = (evt.offsetX - dragStart[0]) * width / canvas.clientWidth
+ const dy = (evt.offsetY - dragStart[1]) * height / canvas.clientHeight
+ if (!(dx === 0 && dy === 0)) {
+ App.preview.get()?.onDrag(dx, dy)
+ redraw()
+ }
}
dragStart = [evt.offsetX, evt.offsetY]
})
;(el as HTMLCanvasElement).addEventListener('mouseup', evt => {
dragStart = undefined
})
-
- view.mount(controlsEl, `
- ${App.preview.get()?.menu(view, redraw) ?? ''}
- ${Octicon.x}
`, false)
})
return ``
}
diff --git a/src/app/preview/BiomeNoisePreview.ts b/src/app/preview/BiomeNoisePreview.ts
index ea8e295c..295220a0 100644
--- a/src/app/preview/BiomeNoisePreview.ts
+++ b/src/app/preview/BiomeNoisePreview.ts
@@ -50,6 +50,10 @@ export class BiomeNoisePreview extends Preview {
`
}
+ getSize(): [number, number] {
+ return [200, 100]
+ }
+
draw(model: DataModel, img: ImageData) {
this.noise = BiomeNoisePreview.noiseMaps.map((id, i) => {
const config = this.state[`${id}_noise`]
@@ -78,9 +82,9 @@ export class BiomeNoisePreview extends Preview {
}
}
- onDrag(fromX: number, fromY: number, toX: number, toY: number) {
- this.offsetX += toX - fromX
- this.offsetY += toY - fromY
+ onDrag(dx: number, dy: number) {
+ this.offsetX += dx
+ this.offsetY += dy
}
private closestBiome(x: number, y: number): string {
diff --git a/src/app/preview/NoiseSettingsPreview.ts b/src/app/preview/NoiseSettingsPreview.ts
index d4db3954..0fac7917 100644
--- a/src/app/preview/NoiseSettingsPreview.ts
+++ b/src/app/preview/NoiseSettingsPreview.ts
@@ -1,17 +1,23 @@
import SimplexNoise from 'simplex-noise'
import { DataModel, Path, ModelPath } from "@mcschema/core"
import { Preview } from './Preview'
-
-const debug = false
+import { toggleMenu, View } from '../views/View'
+import { Octicon } from '../components/Octicon'
export class NoiseSettingsPreview extends Preview {
private noise: SimplexNoise
- private offsetX: number
+ private width: number = 512
+ private chunkWidth: number = 4
+ private chunkHeight: number = 4
+ private chunkCountY: number = 32
+ private offsetX: number = 0
+ private debug: boolean = false
+ private depth: number = 0.1
+ private scale: number = 0.2
constructor() {
super()
this.noise = new SimplexNoise()
- this.offsetX = 0
}
getName() {
@@ -22,14 +28,69 @@ export class NoiseSettingsPreview extends Preview {
return path.endsWith(new Path(['noise']))
}
+ menu(view: View, redraw: () => void) {
+ return ``
+ }
+
+ getSize(): [number, number] {
+ return [this.width, this.state.height]
+ }
+
draw(model: DataModel, img: ImageData) {
+ this.chunkWidth = this.state.size_horizontal * 4
+ this.chunkHeight = this.state.size_vertical * 4
+ this.chunkCountY = Math.floor(this.state.height / this.chunkHeight)
+
const data = img.data
- for (let x = 0; x < 200; x += 1) {
- const densities = this.fillNoiseColumn(x - this.offsetX).reverse()
- for (let y = 0; y < 100; y += 1) {
+ for (let x = 0; x < this.width; x += 1) {
+ const noise = this.iterateNoiseColumn(x - this.offsetX).reverse()
+ for (let y = 0; y < this.state.height; y += 1) {
const i = (y * (img.width * 4)) + (x * 4)
- const color = this.getColor(densities, y)
- data[i] = (debug && densities[y] > 0) ? 255 : color
+ const color = this.getColor(noise, y)
+ data[i] = (this.debug && noise[y] > 0) ? 255 : color
data[i + 1] = color
data[i + 2] = color
data[i + 3] = 255
@@ -37,57 +98,78 @@ export class NoiseSettingsPreview extends Preview {
}
}
- onDrag(fromX: number, fromY: number, toX: number, toY: number) {
- this.offsetX += toX - fromX
+ onDrag(dx: number, dy: number) {
+ this.offsetX += dx
}
- private getColor(densities: number[], y: number): number {
- if (debug) {
- return -densities[y] * 128 + 128
+ private getColor(noise: number[], y: number): number {
+ if (this.debug) {
+ return -noise[y] / 2 + 128
}
- if (densities[y] > 0) {
+ if (noise[y] > 0) {
return 0
}
- if (densities[y+1] > 0) {
+ if (noise[y+1] > 0) {
return 150
}
return 255
}
- private fillNoiseColumn(x: number) {
- const data = Array(100)
- for (let y = 0; y < 100; y += 1) {
- let density = this.getNoise(x, y)
- density = density < -1 ? -1 : density > 1 ? 1 : density
+ private iterateNoiseColumn(x: number): number[] {
+ const data = Array(this.chunkCountY * this.chunkHeight)
+ const cx = Math.floor(x / this.chunkWidth)
+ const ox = Math.floor(x % this.chunkWidth) / this.chunkWidth
+ const noise1 = this.fillNoiseColumn(cx)
+ const noise2 = this.fillNoiseColumn(cx + 1)
- const heightFactor = (1 - y / 50) * this.state.density_factor + this.state.density_offset
- density += heightFactor * (heightFactor > 0 ? 16 : 4)
-
- if (this.state.top_slide.size > 0) {
- density = this.clampedLerp(
- this.state.top_slide.target / 100,
- density,
- (100 - y - (this.state.top_slide.offset * 4)) / (this.state.top_slide.size * 4)
- )
+ for (let y = this.chunkCountY - 1; y >= 0; y -= 1) {
+ for (let yy = this.chunkHeight; yy >= 0; yy -= 1) {
+ const oy = yy / this.chunkHeight
+ const i = y * this.chunkHeight + yy
+ data[i] = this.lerp2(oy, ox, noise1[y], noise1[y+1], noise2[y], noise2[y+1]);
}
-
- if (this.state.bottom_slide.size > 0) {
- density = this.clampedLerp(
- this.state.bottom_slide.target / 100,
- density,
- (y - (this.state.bottom_slide.offset * 4)) / (this.state.bottom_slide.size * 4)
- )
- }
- data[y] = density
}
return data
}
- private getNoise(x: number, y: number) {
+ private fillNoiseColumn(x: number): number[] {
+ const data = Array(this.chunkCountY + 1)
+
+ const scaledDepth = 0.265625 * this.depth
+ const scaledScale = 96 / this.scale
+
+ for (let y = 0; y <= this.chunkCountY; y += 1) {
+ let noise = this.getNoise(x, y)
+ const yOffset = 1 - y * 2 / this.chunkCountY
+ const density = yOffset * this.state.density_factor + this.state.density_offset
+ const falloff = (density + scaledDepth) * scaledScale
+ noise += falloff * (falloff > 0 ? 4 : 1)
+
+ if (this.state.top_slide.size > 0) {
+ noise = this.clampedLerp(
+ this.state.top_slide.target,
+ noise,
+ (this.chunkCountY - y - (this.state.top_slide.offset)) / (this.state.top_slide.size)
+ )
+ }
+
+ if (this.state.bottom_slide.size > 0) {
+ noise = this.clampedLerp(
+ this.state.bottom_slide.target,
+ noise,
+ (y - (this.state.bottom_slide.offset)) / (this.state.bottom_slide.size)
+ )
+ }
+ data[y] = noise
+ }
+ return data
+ }
+
+ private getNoise(x: number, y: number): number {
const octaves = [ [64, 1], [32, 2], [16, 4], [8, 8], [4, 16] ]
return octaves
.map(o => this.noise.noise2D(x / o[0], y / o[0]) / o[1])
- .reduce((prev, acc) => prev + acc)
+ .reduce((prev, acc) => prev + acc) * 256
}
private clampedLerp(a: number, b: number, c: number): number {
@@ -96,7 +178,15 @@ export class NoiseSettingsPreview extends Preview {
} else if (c > 1) {
return b
} else {
- return a + c * (b - a);
+ return this.lerp(c, a, b)
}
}
+
+ public lerp(a: number, b: number, c: number): number {
+ return b + a * (c - b);
+ }
+
+ public lerp2(a: number, b: number, c: number, d: number, e: number, f: number): number {
+ return this.lerp(b, this.lerp(a, c, d), this.lerp(a, e, f));
+ }
}
diff --git a/src/app/preview/Preview.ts b/src/app/preview/Preview.ts
index 7b2c6c79..35b97926 100644
--- a/src/app/preview/Preview.ts
+++ b/src/app/preview/Preview.ts
@@ -13,9 +13,10 @@ export abstract class Preview {
return ''
}
+ abstract getSize(): [number, number]
abstract getName(): string
abstract active(path: ModelPath): boolean
abstract draw(model: DataModel, img: ImageData): void
- onDrag(fromX: number, fromY: number, toX: number, toY: number): void {}
+ onDrag(dx: number, dy: number): void {}
}
diff --git a/src/app/views/View.ts b/src/app/views/View.ts
index b4162350..fdc24525 100644
--- a/src/app/views/View.ts
+++ b/src/app/views/View.ts
@@ -54,8 +54,10 @@ export class View {
}
export const toggleMenu = (el: Element) => {
- el.classList.toggle('active')
+ el.classList.add('active')
document.body.addEventListener('click', evt => {
+ if ((evt.target as Element).matches('.btn.input')) return
+ if ((evt.target as Element).closest('.btn')?.classList.contains('input')) return
el.classList.remove('active')
}, { capture: true, once: true })
}
diff --git a/src/locales/en.json b/src/locales/en.json
index 422eb762..632446f8 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -15,6 +15,10 @@
"title.home": "Data Pack Generators",
"title.suffix": "%0% Minecraft 1.16, 1.17",
"preview": "Visualize",
+ "preview.show_density": "Show Density",
+ "preview.scale": "Scale",
+ "preview.depth": "Depth",
+ "preview.width": "Width",
"undo": "Undo",
"world": "World Settings",
"worldgen/biome": "Biome",
diff --git a/src/styles/global.css b/src/styles/global.css
index b7c536a1..db2fd9c3 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -311,7 +311,7 @@ nav > .toggle span {
transition: background-color var(--style-transition);
}
-.btn:hover {
+.btn:not(.input):hover {
background-color: var(--btn-hover);
}
@@ -335,6 +335,15 @@ nav > .toggle span {
color: var(--btn-active);
}
+.btn.input {
+ cursor: initial;
+}
+
+.btn input {
+ margin-left: 5px;
+ width: 100px;
+}
+
.errors {
position: fixed;
display: flex;