diff --git a/src/drawables/drawable-descriptor.ts b/src/drawables/drawable-descriptor.ts index 6a85929..4ebcd8a 100644 --- a/src/drawables/drawable-descriptor.ts +++ b/src/drawables/drawable-descriptor.ts @@ -7,7 +7,7 @@ import { Drawable } from './drawable'; * Each [[Drawable]] must have a static property of this type, called descriptor. * * For more information on how to create your own DrawableDescriptor-s, look at the - * code of [[Circle]] or [[InvertedTunnel]]. + * code of [[CircleFactory]] or [[InvertedTunnelFactory]]. */ export interface DrawableDescriptor { /** diff --git a/src/drawables/empty-drawable.ts b/src/drawables/empty-drawable.ts new file mode 100644 index 0000000..45a0170 --- /dev/null +++ b/src/drawables/empty-drawable.ts @@ -0,0 +1,18 @@ +import { mat2d, vec2 } from 'gl-matrix'; +import { Drawable } from './drawable'; +import { DrawableDescriptor } from './drawable-descriptor'; + +/** + * @internal + */ +export class EmptyDrawable extends Drawable { + public static readonly descriptor: DrawableDescriptor; + + public minDistance(target: vec2): number { + throw new Error('Unimplemented'); + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + throw new Error('Unimplemented'); + } +} diff --git a/src/drawables/shapes/circle-factory.ts b/src/drawables/shapes/circle-factory.ts new file mode 100644 index 0000000..f611bea --- /dev/null +++ b/src/drawables/shapes/circle-factory.ts @@ -0,0 +1,61 @@ +import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; +import { codeForColorAccess } from '../../helper/code-for-color-access'; +import { DrawableDescriptor } from '../drawable-descriptor'; +import { EmptyDrawable } from '../empty-drawable'; + +/** + * @category Drawable + */ +class CircleBase extends EmptyDrawable { + constructor(public center: vec2, public radius: number) { + super(); + } +} + +/** + * @category Drawable + */ +export const CircleFactory = (color: vec3 | vec4 | number): typeof CircleBase => { + class Circle extends CircleBase { + public static descriptor: DrawableDescriptor = { + sdf: { + shader: ` + uniform vec2 circleCenters[CIRCLE_COUNT]; + uniform float circleRadii[CIRCLE_COUNT]; + + float circleMinDistance(vec2 target, out vec4 color) { + color = ${codeForColorAccess(color)}; + float minDistance = 1000.0; + for (int i = 0; i < CIRCLE_COUNT; i++) { + float dist = distance(circleCenters[i], target) - circleRadii[i]; + minDistance = min(minDistance, dist); + } + + return minDistance; + } + `, + distanceFunctionName: 'circleMinDistance', + }, + propertyUniformMapping: { + center: 'circleCenters', + radius: 'circleRadii', + }, + uniformCountMacroName: 'CIRCLE_COUNT', + shaderCombinationSteps: [0, 1, 2, 3, 8, 16], + empty: new Circle(vec2.create(), 0), + }; + + public minDistance(target: vec2): number { + return vec2.dist(this.center, target) - this.radius; + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + return { + center: vec2.transformMat2d(vec2.create(), this.center, transform2d), + radius: this.radius * transform1d, + }; + } + } + + return Circle; +}; diff --git a/src/drawables/shapes/circle.ts b/src/drawables/shapes/circle.ts deleted file mode 100644 index 4e713bb..0000000 --- a/src/drawables/shapes/circle.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { mat2d, vec2 } from 'gl-matrix'; -import { Drawable } from '../drawable'; -import { DrawableDescriptor } from '../drawable-descriptor'; - -/** - * @category Drawable - */ -export class Circle extends Drawable { - public static descriptor: DrawableDescriptor = { - sdf: { - shader: ` - uniform vec2 circleCenters[CIRCLE_COUNT]; - uniform float circleRadii[CIRCLE_COUNT]; - - float circleMinDistance(vec2 target, out float colorIndex) { - colorIndex = 2.0; - float minDistance = 1000.0; - for (int i = 0; i < CIRCLE_COUNT; i++) { - float dist = distance(circleCenters[i], target) - circleRadii[i]; - minDistance = min(minDistance, dist); - } - - return minDistance; - } - `, - distanceFunctionName: 'circleMinDistance', - }, - propertyUniformMapping: { - center: 'circleCenters', - radius: 'circleRadii', - }, - uniformCountMacroName: 'CIRCLE_COUNT', - shaderCombinationSteps: [0, 1, 2, 3, 8, 16], - empty: new Circle(vec2.create(), 0), - }; - - constructor(public center: vec2, public radius: number) { - super(); - } - - public minDistance(target: vec2): number { - return vec2.dist(this.center, target) - this.radius; - } - - protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { - return { - center: vec2.transformMat2d(vec2.create(), this.center, transform2d), - radius: this.radius * transform1d, - }; - } -} diff --git a/src/drawables/shapes/droplet-factory.ts b/src/drawables/shapes/droplet-factory.ts new file mode 100644 index 0000000..76289a3 --- /dev/null +++ b/src/drawables/shapes/droplet-factory.ts @@ -0,0 +1,100 @@ +import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; +import { clamp01 } from '../../helper/clamp'; +import { codeForColorAccess } from '../../helper/code-for-color-access'; +import { mix } from '../../helper/mix'; +import { DrawableDescriptor } from '../drawable-descriptor'; +import { EmptyDrawable } from '../empty-drawable'; + +/** + * @category Drawable + */ +class DropletBase extends EmptyDrawable { + constructor( + public readonly from: vec2, + public readonly to: vec2, + public readonly fromRadius: number, + public readonly toRadius: number + ) { + super(); + } +} + +/** + * @category Drawable + */ +export const DropletFactory = (color: vec3 | vec4 | number): typeof DropletBase => { + class Droplet extends DropletBase { + public static descriptor: DrawableDescriptor = { + sdf: { + shader: ` + uniform vec2 froms[DROPLET_COUNT]; + uniform vec2 toFromDeltas[DROPLET_COUNT]; + uniform float fromRadii[DROPLET_COUNT]; + uniform float toRadii[DROPLET_COUNT]; + + float dropletMinDistance(vec2 target, out vec4 color) { + color = ${codeForColorAccess(color)}; + + float minDistance = 1000.0; + for (int i = 0; i < DROPLET_COUNT; i++) { + vec2 targetFromDelta = target - froms[i]; + + float h = clamp( + dot(targetFromDelta, toFromDeltas[i]) + / dot(toFromDeltas[i], toFromDeltas[i]), + 0.0, 1.0 + ); + + float currentDistance = -mix( + fromRadii[i], toRadii[i], h + ) + distance( + targetFromDelta, toFromDeltas[i] * h + ); + + minDistance = min(minDistance, currentDistance); + } + + return minDistance; + } + `, + distanceFunctionName: 'dropletMinDistance', + }, + propertyUniformMapping: { + from: 'froms', + toFromDelta: 'toFromDeltas', + fromRadius: 'fromRadii', + toRadius: 'toRadii', + }, + uniformCountMacroName: 'DROPLET_COUNT', + shaderCombinationSteps: [0, 1, 4, 16, 32], + empty: new Droplet(vec2.create(), vec2.create(), 0, 0), + }; + + public minDistance(target: vec2): number { + const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); + const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); + + const h = clamp01( + vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta) + ); + + return ( + vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) - + mix(this.fromRadius, this.toRadius, h) + ); + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); + + return { + from: vec2.transformMat2d(vec2.create(), this.from, transform2d), + toFromDelta: vec2.scale(vec2.create(), toFromDelta, transform1d), + fromRadius: this.fromRadius * transform1d, + toRadius: this.toRadius * transform1d, + }; + } + } + + return Droplet; +}; diff --git a/src/drawables/shapes/droplet.ts b/src/drawables/shapes/droplet.ts deleted file mode 100644 index c35928d..0000000 --- a/src/drawables/shapes/droplet.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { mat2d, vec2 } from 'gl-matrix'; -import { clamp01 } from '../../helper/clamp'; -import { mix } from '../../helper/mix'; -import { Drawable } from '../drawable'; -import { DrawableDescriptor } from '../drawable-descriptor'; - -/** - * @category Drawable - */ -export class Droplet extends Drawable { - public static descriptor: DrawableDescriptor = { - sdf: { - shader: ` - uniform vec2 froms[DROPLET_COUNT]; - uniform vec2 toFromDeltas[DROPLET_COUNT]; - uniform float fromRadii[DROPLET_COUNT]; - uniform float toRadii[DROPLET_COUNT]; - - float dropletMinDistance(vec2 target, out float colorIndex) { - colorIndex = 1.0; - - float minDistance = 1000.0; - for (int i = 0; i < DROPLET_COUNT; i++) { - vec2 targetFromDelta = target - froms[i]; - - float h = clamp( - dot(targetFromDelta, toFromDeltas[i]) - / dot(toFromDeltas[i], toFromDeltas[i]), - 0.0, 1.0 - ); - - float currentDistance = -mix( - fromRadii[i], toRadii[i], h - ) + distance( - targetFromDelta, toFromDeltas[i] * h - ); - - minDistance = min(minDistance, currentDistance); - } - - return minDistance; - } - `, - distanceFunctionName: 'dropletMinDistance', - }, - propertyUniformMapping: { - from: 'froms', - toFromDelta: 'toFromDeltas', - fromRadius: 'fromRadii', - toRadius: 'toRadii', - }, - uniformCountMacroName: 'DROPLET_COUNT', - shaderCombinationSteps: [0, 1, 4, 16, 32], - empty: new Droplet(vec2.create(), vec2.create(), 0, 0), - }; - - constructor( - public readonly from: vec2, - public readonly to: vec2, - public readonly fromRadius: number, - public readonly toRadius: number - ) { - super(); - } - - public minDistance(target: vec2): number { - const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); - const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); - - const h = clamp01( - vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta) - ); - - return ( - vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) - - mix(this.fromRadius, this.toRadius, h) - ); - } - - protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { - const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); - - return { - from: vec2.transformMat2d(vec2.create(), this.from, transform2d), - toFromDelta: vec2.scale(vec2.create(), toFromDelta, transform1d), - fromRadius: this.fromRadius * transform1d, - toRadius: this.toRadius * transform1d, - }; - } -} diff --git a/src/drawables/shapes/inverted-tunnel-factory.ts b/src/drawables/shapes/inverted-tunnel-factory.ts new file mode 100644 index 0000000..e219576 --- /dev/null +++ b/src/drawables/shapes/inverted-tunnel-factory.ts @@ -0,0 +1,123 @@ +import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; +import { clamp01 } from '../../helper/clamp'; +import { codeForColorAccess } from '../../helper/code-for-color-access'; +import { mix } from '../../helper/mix'; +import { DrawableDescriptor } from '../drawable-descriptor'; +import { EmptyDrawable } from '../empty-drawable'; + +/** + * @category Drawable + */ +class InvertedTunnelBase extends EmptyDrawable { + constructor( + public readonly from: vec2, + public readonly to: vec2, + public readonly fromRadius: number, + public readonly toRadius: number + ) { + super(); + } +} + +/** + * Providing a noise texture is required for this drawable. + * + * @category Drawable + */ +export const InvertedTunnelFactory = ( + color: vec3 | vec4 | number +): typeof InvertedTunnelBase => { + class InvertedTunnel extends InvertedTunnelBase { + public static descriptor: DrawableDescriptor = { + sdf: { + shader: ` + uniform vec2 froms[INVERTED_TUNNEL_COUNT]; + uniform vec2 toFromDeltas[INVERTED_TUNNEL_COUNT]; + uniform float fromRadii[INVERTED_TUNNEL_COUNT]; + uniform float toRadii[INVERTED_TUNNEL_COUNT]; + + uniform sampler2D noiseTexture; + + #ifdef WEBGL2_IS_AVAILABLE + float invertedTunnelTerrain(float h) { + return texture( + noiseTexture, + vec2(h, 0.5) + )[0] - 0.5; + } + #else + float invertedTunnelTerrain(float h) { + return texture2D( + noiseTexture, + vec2(h, 0.5) + )[0] - 0.5; + } + #endif + + float invertedTunnelMinDistance(vec2 target, out vec4 color) { + color = ${codeForColorAccess(color)}; + + float minDistance = -1000.0; + for (int i = 0; i < INVERTED_TUNNEL_COUNT; i++) { + + vec2 targetFromDelta = target - froms[i]; + + float h = dot(targetFromDelta, toFromDeltas[i]) + / dot(toFromDeltas[i], toFromDeltas[i]); + + float clampedH = clamp(h, 0.0, 1.0); + + float currentDistance = -mix( + fromRadii[i], toRadii[i], clampedH + ) + distance( + targetFromDelta, toFromDeltas[i] * clampedH + ) - invertedTunnelTerrain(h) / 12.0; + + minDistance = max(minDistance, -currentDistance); + } + + return minDistance; + } + `, + isInverted: true, + distanceFunctionName: 'invertedTunnelMinDistance', + }, + propertyUniformMapping: { + from: 'froms', + toFromDelta: 'toFromDeltas', + fromRadius: 'fromRadii', + toRadius: 'toRadii', + }, + uniformCountMacroName: 'INVERTED_TUNNEL_COUNT', + shaderCombinationSteps: [0, 1, 4, 16, 32], + empty: new InvertedTunnel(vec2.create(), vec2.create(), 0, 0), + }; + + public minDistance(target: vec2): number { + const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); + const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); + + const h = clamp01( + vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta) + ); + + return ( + vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) - + mix(this.fromRadius, this.toRadius, h) + ); + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); + + return { + from: vec2.transformMat2d(vec2.create(), this.from, transform2d), + toFromDelta: vec2.scale(vec2.create(), toFromDelta, transform1d), + fromRadius: this.fromRadius * transform1d, + toRadius: this.toRadius * transform1d, + }; + } + } + + return InvertedTunnel; +}; diff --git a/src/drawables/shapes/inverted-tunnel.ts b/src/drawables/shapes/inverted-tunnel.ts deleted file mode 100644 index bba6c0a..0000000 --- a/src/drawables/shapes/inverted-tunnel.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { mat2d, vec2 } from 'gl-matrix'; -import { clamp01 } from '../../helper/clamp'; -import { mix } from '../../helper/mix'; -import { Drawable } from '../drawable'; -import { DrawableDescriptor } from '../drawable-descriptor'; - -/** - * @category Drawable - * - * Providing a noise texture is required for this drawable. - */ -export class InvertedTunnel extends Drawable { - public static descriptor: DrawableDescriptor = { - sdf: { - shader: ` - uniform vec2 froms[INVERTED_TUNNEL_COUNT]; - uniform vec2 toFromDeltas[INVERTED_TUNNEL_COUNT]; - uniform float fromRadii[INVERTED_TUNNEL_COUNT]; - uniform float toRadii[INVERTED_TUNNEL_COUNT]; - - uniform sampler2D noiseTexture; - - #ifdef WEBGL2_IS_AVAILABLE - float invertedTunnelTerrain(float h) { - return texture( - noiseTexture, - vec2(h, 0.5) - )[0] - 0.5; - } - #else - float invertedTunnelTerrain(float h) { - return texture2D( - noiseTexture, - vec2(h, 0.5) - )[0] - 0.5; - } - #endif - - float invertedTunnelMinDistance(vec2 target, out float colorIndex) { - colorIndex = 3.0; - - float minDistance = -1000.0; - for (int i = 0; i < INVERTED_TUNNEL_COUNT; i++) { - - vec2 targetFromDelta = target - froms[i]; - - float h = dot(targetFromDelta, toFromDeltas[i]) - / dot(toFromDeltas[i], toFromDeltas[i]); - - float clampedH = clamp(h, 0.0, 1.0); - - float currentDistance = -mix( - fromRadii[i], toRadii[i], clampedH - ) + distance( - targetFromDelta, toFromDeltas[i] * clampedH - ) - invertedTunnelTerrain(h) / 12.0; - - minDistance = max(minDistance, -currentDistance); - } - - return minDistance; - } - `, - isInverted: true, - distanceFunctionName: 'invertedTunnelMinDistance', - }, - propertyUniformMapping: { - from: 'froms', - toFromDelta: 'toFromDeltas', - fromRadius: 'fromRadii', - toRadius: 'toRadii', - }, - uniformCountMacroName: 'INVERTED_TUNNEL_COUNT', - shaderCombinationSteps: [0, 1, 4, 16, 32], - empty: new InvertedTunnel(vec2.create(), vec2.create(), 0, 0), - }; - - constructor( - public readonly from: vec2, - public readonly to: vec2, - public readonly fromRadius: number, - public readonly toRadius: number - ) { - super(); - } - - public minDistance(target: vec2): number { - const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); - const targetFromDelta = vec2.subtract(vec2.create(), target, this.from); - - const h = clamp01( - vec2.dot(targetFromDelta, toFromDelta) / vec2.dot(toFromDelta, toFromDelta) - ); - - return ( - vec2.distance(targetFromDelta, vec2.scale(vec2.create(), toFromDelta, h)) - - mix(this.fromRadius, this.toRadius, h) - ); - } - - protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { - const toFromDelta = vec2.subtract(vec2.create(), this.to, this.from); - - return { - from: vec2.transformMat2d(vec2.create(), this.from, transform2d), - toFromDelta: vec2.scale(vec2.create(), toFromDelta, transform1d), - fromRadius: this.fromRadius * transform1d, - toRadius: this.toRadius * transform1d, - }; - } -} diff --git a/src/drawables/shapes/meta-circle-factory.ts b/src/drawables/shapes/meta-circle-factory.ts new file mode 100644 index 0000000..637fa40 --- /dev/null +++ b/src/drawables/shapes/meta-circle-factory.ts @@ -0,0 +1,62 @@ +import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; +import { codeForColorAccess } from '../../helper/code-for-color-access'; +import { DrawableDescriptor } from '../drawable-descriptor'; +import { EmptyDrawable } from '../empty-drawable'; + +/** + * @category Drawable + */ +class MetaCircleBase extends EmptyDrawable { + constructor(public center: vec2, public radius: number) { + super(); + } +} + +/** + * @category Drawable + */ +export const MetaCircleFactory = (color: vec3 | vec4 | number): typeof MetaCircleBase => { + class MetaCircle extends MetaCircleBase { + public static descriptor: DrawableDescriptor = { + sdf: { + shader: ` + uniform vec2 metaCircleCenters[META_CIRCLE_COUNT]; + uniform float metaCircleRadii[META_CIRCLE_COUNT]; + + const float k = 32.0; + float metaCircleMinDistance(vec2 target, out vec4 color) { + color = ${codeForColorAccess(color)}; + float res = 0.0; + for (int i = 0; i < META_CIRCLE_COUNT; i++) { + float dist = distance(metaCircleCenters[i], target) - metaCircleRadii[i]; + res += exp2(-k * dist); + } + + return -log2(res) / k; + } + `, + distanceFunctionName: 'metaCircleMinDistance', + }, + propertyUniformMapping: { + center: 'metaCircleCenters', + radius: 'metaCircleRadii', + }, + uniformCountMacroName: 'META_CIRCLE_COUNT', + shaderCombinationSteps: [0, 1, 2, 3, 8, 16], + empty: new MetaCircle(vec2.create(), 0), + }; + + public minDistance(target: vec2): number { + return vec2.dist(this.center, target) - this.radius * 2; + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + return { + center: vec2.transformMat2d(vec2.create(), this.center, transform2d), + radius: this.radius * transform1d, + }; + } + } + + return MetaCircle; +}; diff --git a/src/drawables/shapes/meta-circle.ts b/src/drawables/shapes/meta-circle.ts deleted file mode 100644 index 0c7c848..0000000 --- a/src/drawables/shapes/meta-circle.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { mat2d, vec2 } from 'gl-matrix'; -import { Drawable } from '../drawable'; -import { DrawableDescriptor } from '../drawable-descriptor'; - -/** - * @category Drawable - */ -export class MetaCircle extends Drawable { - public static descriptor: DrawableDescriptor = { - sdf: { - shader: ` - uniform vec2 metaCircleCenters[META_CIRCLE_COUNT]; - uniform float metaCircleRadii[META_CIRCLE_COUNT]; - - const float k = 32.0; - float metaCircleMinDistance(vec2 target, out float colorIndex) { - colorIndex = 5.0; - float res = 0.0; - for (int i = 0; i < META_CIRCLE_COUNT; i++) { - float dist = distance(metaCircleCenters[i], target) - metaCircleRadii[i]; - res += exp2(-k * dist); - } - - return -log2(res) / k; - } - `, - distanceFunctionName: 'metaCircleMinDistance', - }, - propertyUniformMapping: { - center: 'metaCircleCenters', - radius: 'metaCircleRadii', - }, - uniformCountMacroName: 'META_CIRCLE_COUNT', - shaderCombinationSteps: [0, 1, 2, 3, 8, 16], - empty: new MetaCircle(vec2.create(), 0), - }; - - constructor(public center: vec2, public radius: number) { - super(); - } - - public minDistance(target: vec2): number { - return vec2.dist(this.center, target) - this.radius * 2; - } - - protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { - return { - center: vec2.transformMat2d(vec2.create(), this.center, transform2d), - radius: this.radius * transform1d, - }; - } -} diff --git a/src/drawables/shapes/noisy-polygon-factory.ts b/src/drawables/shapes/noisy-polygon-factory.ts new file mode 100644 index 0000000..52cef59 --- /dev/null +++ b/src/drawables/shapes/noisy-polygon-factory.ts @@ -0,0 +1,169 @@ +import { mat2d, vec2 } from 'gl-matrix'; +import { Drawable } from '../drawable'; +import { DrawableDescriptor } from '../drawable-descriptor'; +import { PolygonBase, PolygonFactory } from './polygon-factory'; + +interface NoisyPolygonBase extends PolygonBase { + randomOffset: number; +} +/** + * @category Drawable + */ +export const NoisyPolygonFactory = ( + vertexCount: number, + colorIndex: number +): typeof PolygonBase & NoisyPolygonBase => { + class NoisyPolygon extends PolygonFactory(vertexCount, colorIndex) { + public static descriptor: DrawableDescriptor = { + sdf: { + shader: ` + uniform vec2 noisyPolygon${vertexCount}Vertices[NOISY_POLGYON${vertexCount}_COUNT * ${vertexCount}]; + uniform vec2 noisyPolygon${vertexCount}Centers[NOISY_POLGYON${vertexCount}_COUNT]; + uniform float noisyPolygon${vertexCount}Lengths[NOISY_POLGYON${vertexCount}_COUNT]; + uniform float noisyPolygon${vertexCount}Randoms[NOISY_POLGYON${vertexCount}_COUNT]; + + uniform sampler2D noiseTexture; + + #ifdef WEBGL2_IS_AVAILABLE + float myTerrain(vec2 h) { + return texture(noiseTexture, h)[0] - 0.5; + } + #else + float myTerrain(vec2 h) { + return texture2D(noiseTexture, h)[0] - 0.5; + } + #endif + + vec2 noisyPolygon${vertexCount}LineDistance(vec2 target, vec2 from, vec2 to) { + vec2 targetFromDelta = target - from; + vec2 toFromDelta = to - from; + float h = clamp( + dot(targetFromDelta, toFromDelta) / dot(toFromDelta, toFromDelta), + 0.0, 1.0 + ); + + vec2 diff = targetFromDelta - toFromDelta * h; + return vec2( + dot(diff, diff), + toFromDelta.x * targetFromDelta.y - toFromDelta.y * targetFromDelta.x + ); + } + + float noisyPolygon${vertexCount}MinDistance(vec2 target, out vec4 color) { + color = readFromPalette(${colorIndex}); + + float minDistance = 100.0; + + for (int j = 0; j < NOISY_POLGYON${vertexCount}_COUNT; j++) { + vec2 startEnd = noisyPolygon${vertexCount}Vertices[j * ${vertexCount}]; + vec2 vb = startEnd; + + vec2 center = noisyPolygon${vertexCount}Centers[j]; + float l = noisyPolygon${vertexCount}Lengths[j]; + float randomOffset = noisyPolygon${vertexCount}Randoms[j]; + vec2 targetTangent = normalize(target - center); + vec2 noisyTarget = target - ( + targetTangent * myTerrain(vec2( + l * abs(atan(targetTangent.y, targetTangent.x)), + randomOffset + )) / 12.0 + ); + + float d = 10000.0; + float s = 1.0; + for (int k = 1; k < ${vertexCount}; k++) { + vec2 va = vb; + vb = noisyPolygon${vertexCount}Vertices[j * ${vertexCount} + k]; + vec2 ds = noisyPolygon${vertexCount}LineDistance(noisyTarget, va, vb); + + bvec3 cond = bvec3(noisyTarget.y >= va.y, noisyTarget.y < vb.y, ds.y > 0.0); + if (all(cond) || all(not(cond))) { + s *= -1.0; + } + + d = min(d, ds.x); + } + + vec2 ds = noisyPolygon${vertexCount}LineDistance(noisyTarget, vb, startEnd); + bvec3 cond = bvec3(noisyTarget.y >= vb.y, noisyTarget.y < startEnd.y, ds.y > 0.0); + if (all(cond) || all(not(cond))) { + s *= -1.0; + } + + d = min(d, ds.x); + minDistance = min(minDistance, s * sqrt(d)); + } + + return minDistance; + } + `, + distanceFunctionName: `noisyPolygon${vertexCount}MinDistance`, + }, + propertyUniformMapping: { + vertices: `noisyPolygon${vertexCount}Vertices`, + length: `noisyPolygon${vertexCount}Lengths`, + random: `noisyPolygon${vertexCount}Randoms`, + center: `noisyPolygon${vertexCount}Centers`, + }, + objectCountScaler: 1 / vertexCount, + uniformCountMacroName: `NOISY_POLGYON${vertexCount}_COUNT`, + shaderCombinationSteps: [0, 1, 2, 3, 8, 16], + empty: (new NoisyPolygon( + new Array(vertexCount).fill(vec2.create()) + ) as unknown) as Drawable, + }; + + public randomOffset = 0; + + constructor(public vertices: Array) { + super(vertices); + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + const transformedVertices = (this as any).actualVertices.map((v: vec2) => + vec2.transformMat2d(vec2.create(), v, transform2d) + ); + + const center = transformedVertices.reduce( + (sum: vec2, v: vec2) => vec2.add(sum, sum, v), + vec2.create() + ); + vec2.scale(center, center, 1 / transformedVertices.length); + + let length = 0; + for (let i = 1; i < this.vertices.length; i++) { + length += vec2.distance(transformedVertices[i - 1], transformedVertices[i]); + } + + return { + vertices: transformedVertices, + center, + length, + random: this.randomOffset, + }; + } + + public serializeToUniforms( + uniforms: any, + transform2d: mat2d, + transform1d: number + ): void { + const { propertyUniformMapping } = (this.constructor as typeof Drawable).descriptor; + + const serialized = this.getObjectToSerialize(transform2d, transform1d); + Object.entries(propertyUniformMapping).forEach(([k, v]) => { + if (!Object.prototype.hasOwnProperty.call(uniforms, v)) { + uniforms[v] = []; + } + + if (k === 'vertices') { + uniforms[v].push(...serialized[k]); + } else { + uniforms[v].push(serialized[k]); + } + }); + } + } + + return NoisyPolygon as any; +}; diff --git a/src/drawables/shapes/polygon-factory.ts b/src/drawables/shapes/polygon-factory.ts index 4f2563f..3cc45ce 100644 --- a/src/drawables/shapes/polygon-factory.ts +++ b/src/drawables/shapes/polygon-factory.ts @@ -2,14 +2,25 @@ import { mat2d, vec2 } from 'gl-matrix'; import { clamp01 } from '../../helper/clamp'; import { Drawable } from '../drawable'; import { DrawableDescriptor } from '../drawable-descriptor'; +import { EmptyDrawable } from '../empty-drawable'; + +/** + * @category Drawable + */ +export class PolygonBase extends EmptyDrawable { + constructor(public vertices: Array) { + super(); + } +} /** * @category Drawable */ export const PolygonFactory = ( - vertexCount: number -): { new (vertices: Array): Drawable; descriptor: DrawableDescriptor } => { - class Polygon extends Drawable { + vertexCount: number, + colorIndex: number +): typeof PolygonBase => { + class Polygon extends PolygonBase { public static descriptor: DrawableDescriptor = { sdf: { shader: ` @@ -30,8 +41,9 @@ export const PolygonFactory = ( ); } - float polygon${vertexCount}MinDistance(vec2 target, out float colorIndex) { - colorIndex = 1.0; + float polygon${vertexCount}MinDistance(vec2 target, out vec4 color) { + color = readFromPalette(${colorIndex}); + float minDistance = 100.0; for (int j = 0; j < POLGYON${vertexCount}_COUNT; j++) { @@ -54,7 +66,6 @@ export const PolygonFactory = ( } vec2 ds = polygon${vertexCount}LineDistance(target, vb, startEnd); - bvec3 cond = bvec3(target.y >= vb.y, target.y < startEnd.y, ds.y > 0.0); if (all(cond) || all(not(cond))) { s *= -1.0; @@ -78,8 +89,8 @@ export const PolygonFactory = ( empty: new Polygon(new Array(vertexCount).fill(vec2.create())), }; - constructor(public vertices: Array) { - super(); + constructor(vertices: Array) { + super(vertices); if (vertices.length > vertexCount) { throw new Error( diff --git a/src/drawables/shapes/rotated-rectangle-factory.ts b/src/drawables/shapes/rotated-rectangle-factory.ts new file mode 100644 index 0000000..c740ece --- /dev/null +++ b/src/drawables/shapes/rotated-rectangle-factory.ts @@ -0,0 +1,91 @@ +import { mat2d, vec2, vec3, vec4 } from 'gl-matrix'; +import { codeForColorAccess } from '../../helper/code-for-color-access'; +import { DrawableDescriptor } from '../drawable-descriptor'; +import { EmptyDrawable } from '../empty-drawable'; + +/** + * @category Drawable + */ +class RotatedRectangleBase extends EmptyDrawable { + constructor(public center: vec2, public size: vec2, public rotation: number) { + super(); + } +} + +/** + * @category Drawable + */ +export const RotatedRectangleFactory = ( + color: vec3 | vec4 | number +): typeof RotatedRectangleBase => { + class RotatedRectangle extends RotatedRectangleBase { + public static descriptor: DrawableDescriptor = { + sdf: { + // Source: https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm + shader: ` + uniform vec2 rotatedRectangleTopCenters[ROTATED_RECTANGLE_COUNT]; + uniform vec2 rotatedRectangleBottomCenters[ROTATED_RECTANGLE_COUNT]; + uniform float rotatedRectangleWidths[ROTATED_RECTANGLE_COUNT]; + + float rotatedRectangleMinDistance(vec2 target, out vec4 color) { + color = ${codeForColorAccess(color)}; + + float minDistance = 1000.0; + for (int i = 0; i < ROTATED_RECTANGLE_COUNT; i++) { + vec2 top = rotatedRectangleTopCenters[i]; + vec2 bottom = rotatedRectangleBottomCenters[i]; + float height = length(bottom - top); + vec2 d = (bottom - top) / height; + + vec2 q = (target - (top + bottom) * 0.5); + q = mat2(d.x, -d.y, d.y, d.x) * q; + q = abs(q) - vec2(height, rotatedRectangleWidths[i]) * 0.5; + float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0); + minDistance = min(minDistance, dist); + } + + return minDistance; + } + `, + distanceFunctionName: 'rotatedRectangleMinDistance', + }, + propertyUniformMapping: { + topCenter: 'rotatedRectangleTopCenters', + bottomCenter: 'rotatedRectangleBottomCenters', + width: 'rotatedRectangleWidths', + }, + uniformCountMacroName: 'ROTATED_RECTANGLE_COUNT', + shaderCombinationSteps: [0, 1, 2, 3, 8, 16], + empty: new RotatedRectangle(vec2.create(), vec2.create(), 0), + }; + + /** + * It is just an estimate by calculating a bounding circle + * @param target + */ + public minDistance(target: vec2): number { + return vec2.distance(this.center, target) - vec2.length(this.size) / 2; + } + + protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { + const rotation = mat2d.fromRotation(mat2d.create(), this.rotation); + + const halfHeight = vec2.fromValues(0, this.size.y / 2); + vec2.transformMat2d(halfHeight, halfHeight, rotation); + + const topCenter = vec2.add(vec2.create(), this.center, halfHeight); + vec2.transformMat2d(topCenter, topCenter, transform2d); + + const bottomCenter = vec2.subtract(vec2.create(), this.center, halfHeight); + vec2.transformMat2d(bottomCenter, bottomCenter, transform2d); + + return { + topCenter, + bottomCenter, + width: this.size.x * transform1d, + }; + } + } + + return RotatedRectangle; +}; diff --git a/src/drawables/shapes/rotated-rectangle.ts b/src/drawables/shapes/rotated-rectangle.ts deleted file mode 100644 index b5cc75d..0000000 --- a/src/drawables/shapes/rotated-rectangle.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { mat2d, vec2 } from 'gl-matrix'; -import { Drawable } from '../drawable'; -import { DrawableDescriptor } from '../drawable-descriptor'; - -/** - * @category Drawable - */ -export class RotatedRectangle extends Drawable { - public static descriptor: DrawableDescriptor = { - sdf: { - // Source: https://iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm - shader: ` - uniform vec2 rotatedRectangleTopCenters[ROTATED_RECTANGLE_COUNT]; - uniform vec2 rotatedRectangleBottomCenters[ROTATED_RECTANGLE_COUNT]; - uniform float rotatedRectangleWidths[ROTATED_RECTANGLE_COUNT]; - - float rotatedRectangleMinDistance(vec2 target, out float colorIndex) { - colorIndex = 4.0; - float minDistance = 1000.0; - for (int i = 0; i < ROTATED_RECTANGLE_COUNT; i++) { - vec2 top = rotatedRectangleTopCenters[i]; - vec2 bottom = rotatedRectangleBottomCenters[i]; - float height = length(bottom - top); - vec2 d = (bottom - top) / height; - - vec2 q = (target - (top + bottom) * 0.5); - q = mat2(d.x, -d.y, d.y, d.x) * q; - q = abs(q) - vec2(height, rotatedRectangleWidths[i]) * 0.5; - float dist = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0); - minDistance = min(minDistance, dist); - } - - return minDistance; - } - `, - distanceFunctionName: 'rotatedRectangleMinDistance', - }, - propertyUniformMapping: { - topCenter: 'rotatedRectangleTopCenters', - bottomCenter: 'rotatedRectangleBottomCenters', - width: 'rotatedRectangleWidths', - }, - uniformCountMacroName: 'ROTATED_RECTANGLE_COUNT', - shaderCombinationSteps: [0, 1, 2, 3, 8, 16], - empty: new RotatedRectangle(vec2.create(), vec2.create(), 0), - }; - - constructor(public center: vec2, public size: vec2, public rotation: number) { - super(); - } - - /** - * It is just an estimate by calculating a bounding circle - * @param target - */ - public minDistance(target: vec2): number { - return vec2.distance(this.center, target) - vec2.length(this.size) / 2; - } - - protected getObjectToSerialize(transform2d: mat2d, transform1d: number): any { - const rotation = mat2d.fromRotation(mat2d.create(), this.rotation); - - const halfHeight = vec2.fromValues(0, this.size.y / 2); - vec2.transformMat2d(halfHeight, halfHeight, rotation); - - const topCenter = vec2.add(vec2.create(), this.center, halfHeight); - vec2.transformMat2d(topCenter, topCenter, transform2d); - - const bottomCenter = vec2.subtract(vec2.create(), this.center, halfHeight); - vec2.transformMat2d(bottomCenter, bottomCenter, transform2d); - - return { - topCenter, - bottomCenter, - width: this.size.x * transform1d, - }; - } -}