@joydle/parallax 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/generators.d.ts +11 -0
- package/dist/core/generators.js +53 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +2 -0
- package/dist/core/types.d.ts +74 -0
- package/dist/core/types.js +10 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/pixi/PixiParallaxLayer.d.ts +25 -0
- package/dist/pixi/PixiParallaxLayer.js +77 -0
- package/dist/pixi/PixiParallaxStack.d.ts +31 -0
- package/dist/pixi/PixiParallaxStack.js +75 -0
- package/dist/pixi/index.d.ts +4 -0
- package/dist/pixi/index.js +2 -0
- package/dist/three/ThreeParallaxLayer.d.ts +46 -0
- package/dist/three/ThreeParallaxLayer.js +84 -0
- package/dist/three/ThreeParallaxStack.d.ts +42 -0
- package/dist/three/ThreeParallaxStack.js +85 -0
- package/dist/three/index.d.ts +4 -0
- package/dist/three/index.js +2 -0
- package/package.json +58 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GradientConfig, StarfieldConfig } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a gradient canvas.
|
|
4
|
+
* Returns an OffscreenCanvas (or regular Canvas as fallback).
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateGradient(width: number, height: number, config: GradientConfig): HTMLCanvasElement;
|
|
7
|
+
/**
|
|
8
|
+
* Generate a starfield canvas with random star positions.
|
|
9
|
+
* Stars are drawn as small bright dots with varying opacity.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateStarfield(width: number, height: number, config: StarfieldConfig): HTMLCanvasElement;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a gradient canvas.
|
|
3
|
+
* Returns an OffscreenCanvas (or regular Canvas as fallback).
|
|
4
|
+
*/
|
|
5
|
+
export function generateGradient(width, height, config) {
|
|
6
|
+
const canvas = document.createElement('canvas');
|
|
7
|
+
canvas.width = width;
|
|
8
|
+
canvas.height = height;
|
|
9
|
+
const ctx = canvas.getContext('2d');
|
|
10
|
+
const isVertical = (config.direction ?? 'vertical') === 'vertical';
|
|
11
|
+
const grad = isVertical
|
|
12
|
+
? ctx.createLinearGradient(0, 0, 0, height)
|
|
13
|
+
: ctx.createLinearGradient(0, 0, width, 0);
|
|
14
|
+
const colors = config.colors;
|
|
15
|
+
for (let i = 0; i < colors.length; i++) {
|
|
16
|
+
grad.addColorStop(i / Math.max(colors.length - 1, 1), colors[i]);
|
|
17
|
+
}
|
|
18
|
+
ctx.fillStyle = grad;
|
|
19
|
+
ctx.fillRect(0, 0, width, height);
|
|
20
|
+
return canvas;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Generate a starfield canvas with random star positions.
|
|
24
|
+
* Stars are drawn as small bright dots with varying opacity.
|
|
25
|
+
*/
|
|
26
|
+
export function generateStarfield(width, height, config) {
|
|
27
|
+
const canvas = document.createElement('canvas');
|
|
28
|
+
canvas.width = width;
|
|
29
|
+
canvas.height = height;
|
|
30
|
+
const ctx = canvas.getContext('2d');
|
|
31
|
+
// Background
|
|
32
|
+
const bg = config.background ?? 'transparent';
|
|
33
|
+
if (bg !== 'transparent') {
|
|
34
|
+
ctx.fillStyle = bg;
|
|
35
|
+
ctx.fillRect(0, 0, width, height);
|
|
36
|
+
}
|
|
37
|
+
const density = config.density ?? 150;
|
|
38
|
+
const color = config.color ?? '#ffffff';
|
|
39
|
+
// Seeded-ish random for consistency within a session
|
|
40
|
+
for (let i = 0; i < density; i++) {
|
|
41
|
+
const x = Math.random() * width;
|
|
42
|
+
const y = Math.random() * height;
|
|
43
|
+
const radius = 0.5 + Math.random() * 1.5;
|
|
44
|
+
const alpha = 0.3 + Math.random() * 0.7;
|
|
45
|
+
ctx.beginPath();
|
|
46
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
47
|
+
ctx.fillStyle = color;
|
|
48
|
+
ctx.globalAlpha = alpha;
|
|
49
|
+
ctx.fill();
|
|
50
|
+
}
|
|
51
|
+
ctx.globalAlpha = 1;
|
|
52
|
+
return canvas;
|
|
53
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { generateGradient, generateStarfield } from './generators';
|
|
2
|
+
export { distributeSpeed } from './types';
|
|
3
|
+
export type { LayerConfig, TextureLayerConfig, ProceduralLayerConfig, ProceduralType, GradientConfig, StarfieldConfig, StackConfig, ParallaxStack, } from './types';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Configuration for a single parallax layer */
|
|
2
|
+
export interface LayerConfig {
|
|
3
|
+
/** Unique identifier for runtime manipulation */
|
|
4
|
+
key: string;
|
|
5
|
+
/** Scroll speed relative to camera. 0 = static, 1 = moves with camera. */
|
|
6
|
+
speed?: number;
|
|
7
|
+
/** Repeat direction. Default 'x' */
|
|
8
|
+
repeat?: 'x' | 'y' | 'both' | 'none';
|
|
9
|
+
/** Render order — lower = further back. Auto-assigned if omitted. */
|
|
10
|
+
zIndex?: number;
|
|
11
|
+
/** Override layer width (defaults to viewport width × 3) */
|
|
12
|
+
width?: number;
|
|
13
|
+
/** Override layer height (defaults to viewport height × 3) */
|
|
14
|
+
height?: number;
|
|
15
|
+
}
|
|
16
|
+
/** A layer backed by a texture/image */
|
|
17
|
+
export interface TextureLayerConfig extends LayerConfig {
|
|
18
|
+
texture: unknown;
|
|
19
|
+
procedural?: never;
|
|
20
|
+
}
|
|
21
|
+
/** A layer generated procedurally (no image asset needed) */
|
|
22
|
+
export interface ProceduralLayerConfig extends LayerConfig {
|
|
23
|
+
texture?: never;
|
|
24
|
+
procedural: ProceduralType;
|
|
25
|
+
}
|
|
26
|
+
export type ProceduralType = GradientConfig | StarfieldConfig;
|
|
27
|
+
export interface GradientConfig {
|
|
28
|
+
type: 'gradient';
|
|
29
|
+
/** Array of CSS color strings, top to bottom */
|
|
30
|
+
colors: string[];
|
|
31
|
+
/** Gradient direction. Default 'vertical' */
|
|
32
|
+
direction?: 'vertical' | 'horizontal';
|
|
33
|
+
}
|
|
34
|
+
export interface StarfieldConfig {
|
|
35
|
+
type: 'starfield';
|
|
36
|
+
/** Number of stars. Default 150 */
|
|
37
|
+
density?: number;
|
|
38
|
+
/** Enable twinkle animation. Default true */
|
|
39
|
+
twinkle?: boolean;
|
|
40
|
+
/** Star color. Default '#ffffff' */
|
|
41
|
+
color?: string;
|
|
42
|
+
/** Background color. Default 'transparent' */
|
|
43
|
+
background?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Configuration for a parallax stack */
|
|
46
|
+
export interface StackConfig<L extends LayerConfig = LayerConfig> {
|
|
47
|
+
/** Viewport dimensions — used for layer sizing */
|
|
48
|
+
viewport?: {
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
};
|
|
52
|
+
/** If true, speeds are auto-distributed (back=0.05 → front=0.7). Layer order = back to front. */
|
|
53
|
+
autoSpeed?: boolean;
|
|
54
|
+
/** Layer definitions, ordered back to front */
|
|
55
|
+
layers: L[];
|
|
56
|
+
}
|
|
57
|
+
/** Shared interface for all parallax stack implementations */
|
|
58
|
+
export interface ParallaxStack {
|
|
59
|
+
/** Update all layers based on camera position */
|
|
60
|
+
update(camX: number, camY: number, zoom?: number): void;
|
|
61
|
+
/** Change a layer's speed at runtime */
|
|
62
|
+
setSpeed(key: string, speed: number): void;
|
|
63
|
+
/** Add a layer at runtime */
|
|
64
|
+
addLayer(config: LayerConfig): void;
|
|
65
|
+
/** Remove a layer by key */
|
|
66
|
+
removeLayer(key: string): void;
|
|
67
|
+
/** Clean up all layers and resources */
|
|
68
|
+
destroy(): void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Distribute speeds across N layers from back (slow) to front (fast).
|
|
72
|
+
* Range: 0.05 → 0.7
|
|
73
|
+
*/
|
|
74
|
+
export declare function distributeSpeed(index: number, total: number): number;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { generateGradient, generateStarfield } from './core/generators';
|
|
2
|
+
export { distributeSpeed } from './core/types';
|
|
3
|
+
export type { LayerConfig, TextureLayerConfig, ProceduralLayerConfig, ProceduralType, GradientConfig, StarfieldConfig, StackConfig, ParallaxStack, } from './core/types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Container, Sprite, TilingSprite, Texture } from 'pixi.js';
|
|
2
|
+
import type { TextureLayerConfig, ProceduralLayerConfig } from '../core/types';
|
|
3
|
+
export type PixiLayerConfig = PixiTextureLayerConfig | PixiProceduralLayerConfig;
|
|
4
|
+
export interface PixiTextureLayerConfig extends Omit<TextureLayerConfig, 'texture'> {
|
|
5
|
+
texture: Texture;
|
|
6
|
+
procedural?: never;
|
|
7
|
+
}
|
|
8
|
+
export interface PixiProceduralLayerConfig extends ProceduralLayerConfig {
|
|
9
|
+
}
|
|
10
|
+
export declare class PixiParallaxLayer {
|
|
11
|
+
readonly sprite: Sprite | TilingSprite;
|
|
12
|
+
readonly key: string;
|
|
13
|
+
private _speed;
|
|
14
|
+
private _twinkleTime;
|
|
15
|
+
private _twinkleEnabled;
|
|
16
|
+
get speed(): number;
|
|
17
|
+
set speed(v: number);
|
|
18
|
+
constructor(parent: Container, config: PixiLayerConfig, viewport: {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
});
|
|
22
|
+
/** Update parallax position based on camera world coordinates */
|
|
23
|
+
update(camX: number, camY: number, _zoom?: number, dt?: number): void;
|
|
24
|
+
destroy(): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Sprite, TilingSprite, Texture } from 'pixi.js';
|
|
2
|
+
import { generateGradient, generateStarfield } from '../core/generators';
|
|
3
|
+
export class PixiParallaxLayer {
|
|
4
|
+
sprite;
|
|
5
|
+
key;
|
|
6
|
+
_speed;
|
|
7
|
+
_twinkleTime = 0;
|
|
8
|
+
_twinkleEnabled = false;
|
|
9
|
+
get speed() {
|
|
10
|
+
return this._speed;
|
|
11
|
+
}
|
|
12
|
+
set speed(v) {
|
|
13
|
+
this._speed = v;
|
|
14
|
+
}
|
|
15
|
+
constructor(parent, config, viewport) {
|
|
16
|
+
this.key = config.key;
|
|
17
|
+
this._speed = config.speed ?? 0.3;
|
|
18
|
+
const w = config.width ?? viewport.width * 3;
|
|
19
|
+
const h = config.height ?? viewport.height * 3;
|
|
20
|
+
let texture;
|
|
21
|
+
if (config.procedural) {
|
|
22
|
+
// Generate procedural texture
|
|
23
|
+
const proc = config.procedural;
|
|
24
|
+
let canvas;
|
|
25
|
+
if (proc.type === 'gradient') {
|
|
26
|
+
canvas = generateGradient(Math.min(w, 512), Math.min(h, 512), proc);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
canvas = generateStarfield(Math.min(w, 512), Math.min(h, 512), proc);
|
|
30
|
+
this._twinkleEnabled = proc.twinkle !== false;
|
|
31
|
+
}
|
|
32
|
+
texture = Texture.from(canvas);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
texture = config.texture;
|
|
36
|
+
}
|
|
37
|
+
const repeatX = config.repeat === 'x' || config.repeat === 'both';
|
|
38
|
+
const repeatY = config.repeat === 'y' || config.repeat === 'both';
|
|
39
|
+
if (repeatX || repeatY) {
|
|
40
|
+
this.sprite = new TilingSprite({ texture, width: w, height: h });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.sprite = new Sprite(texture);
|
|
44
|
+
if (config.width)
|
|
45
|
+
this.sprite.width = w;
|
|
46
|
+
if (config.height)
|
|
47
|
+
this.sprite.height = h;
|
|
48
|
+
}
|
|
49
|
+
this.sprite.zIndex = config.zIndex ?? -1;
|
|
50
|
+
parent.addChild(this.sprite);
|
|
51
|
+
parent.sortableChildren = true;
|
|
52
|
+
}
|
|
53
|
+
/** Update parallax position based on camera world coordinates */
|
|
54
|
+
update(camX, camY, _zoom, dt) {
|
|
55
|
+
const dx = camX * this._speed;
|
|
56
|
+
const dy = camY * this._speed;
|
|
57
|
+
if (this.sprite instanceof TilingSprite) {
|
|
58
|
+
this.sprite.tilePosition.x = -dx;
|
|
59
|
+
this.sprite.tilePosition.y = -dy;
|
|
60
|
+
this.sprite.x = camX - this.sprite.width / 2;
|
|
61
|
+
this.sprite.y = camY - this.sprite.height / 2;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.sprite.x = camX * (1 - this._speed);
|
|
65
|
+
this.sprite.y = camY * (1 - this._speed);
|
|
66
|
+
}
|
|
67
|
+
// Twinkle effect for starfield layers
|
|
68
|
+
if (this._twinkleEnabled && dt) {
|
|
69
|
+
this._twinkleTime += dt;
|
|
70
|
+
this.sprite.alpha = 0.85 + Math.sin(this._twinkleTime * 1.5) * 0.15;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
destroy() {
|
|
74
|
+
this.sprite.removeFromParent();
|
|
75
|
+
this.sprite.destroy();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Container } from 'pixi.js';
|
|
2
|
+
import type { ParallaxStack, StackConfig } from '../core/types';
|
|
3
|
+
import { PixiParallaxLayer } from './PixiParallaxLayer';
|
|
4
|
+
import type { PixiLayerConfig } from './PixiParallaxLayer';
|
|
5
|
+
export interface PixiStackConfig extends StackConfig<PixiLayerConfig> {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Multi-layer parallax manager for PixiJS.
|
|
9
|
+
*
|
|
10
|
+
* Manages creation, ordering, updating, and destruction of parallax layers.
|
|
11
|
+
* Supports texture-based and procedural (gradient, starfield) layers.
|
|
12
|
+
*/
|
|
13
|
+
export declare class PixiParallaxStack implements ParallaxStack {
|
|
14
|
+
private parent;
|
|
15
|
+
private layers;
|
|
16
|
+
private config;
|
|
17
|
+
private viewport;
|
|
18
|
+
constructor(parent: Container, config: PixiStackConfig);
|
|
19
|
+
/** Update all layers. Call after cam.update(dt), pass cam.x, cam.y. */
|
|
20
|
+
update(camX: number, camY: number, zoom?: number, dt?: number): void;
|
|
21
|
+
/** Change a layer's scroll speed at runtime */
|
|
22
|
+
setSpeed(key: string, speed: number): void;
|
|
23
|
+
/** Add a new layer at runtime */
|
|
24
|
+
addLayer(config: PixiLayerConfig): void;
|
|
25
|
+
/** Remove a layer by key */
|
|
26
|
+
removeLayer(key: string): void;
|
|
27
|
+
/** Get a layer by key (for direct manipulation) */
|
|
28
|
+
getLayer(key: string): PixiParallaxLayer | undefined;
|
|
29
|
+
/** Clean up all layers */
|
|
30
|
+
destroy(): void;
|
|
31
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { distributeSpeed } from '../core/types';
|
|
2
|
+
import { PixiParallaxLayer } from './PixiParallaxLayer';
|
|
3
|
+
/**
|
|
4
|
+
* Multi-layer parallax manager for PixiJS.
|
|
5
|
+
*
|
|
6
|
+
* Manages creation, ordering, updating, and destruction of parallax layers.
|
|
7
|
+
* Supports texture-based and procedural (gradient, starfield) layers.
|
|
8
|
+
*/
|
|
9
|
+
export class PixiParallaxStack {
|
|
10
|
+
parent;
|
|
11
|
+
layers = new Map();
|
|
12
|
+
config;
|
|
13
|
+
viewport;
|
|
14
|
+
constructor(parent, config) {
|
|
15
|
+
this.parent = parent;
|
|
16
|
+
this.config = config;
|
|
17
|
+
this.viewport = config.viewport ?? { width: 480, height: 700 };
|
|
18
|
+
const total = config.layers.length;
|
|
19
|
+
for (let i = 0; i < total; i++) {
|
|
20
|
+
const layerCfg = { ...config.layers[i] };
|
|
21
|
+
// Auto-assign speed if autoSpeed enabled and no explicit speed
|
|
22
|
+
if (config.autoSpeed && layerCfg.speed === undefined) {
|
|
23
|
+
layerCfg.speed = distributeSpeed(i, total);
|
|
24
|
+
}
|
|
25
|
+
// Auto-assign zIndex based on order (back to front)
|
|
26
|
+
if (layerCfg.zIndex === undefined) {
|
|
27
|
+
layerCfg.zIndex = -100 + i * 10;
|
|
28
|
+
}
|
|
29
|
+
const layer = new PixiParallaxLayer(parent, layerCfg, this.viewport);
|
|
30
|
+
this.layers.set(layerCfg.key, layer);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Update all layers. Call after cam.update(dt), pass cam.x, cam.y. */
|
|
34
|
+
update(camX, camY, zoom, dt) {
|
|
35
|
+
for (const layer of this.layers.values()) {
|
|
36
|
+
layer.update(camX, camY, zoom, dt);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Change a layer's scroll speed at runtime */
|
|
40
|
+
setSpeed(key, speed) {
|
|
41
|
+
const layer = this.layers.get(key);
|
|
42
|
+
if (layer)
|
|
43
|
+
layer.speed = speed;
|
|
44
|
+
}
|
|
45
|
+
/** Add a new layer at runtime */
|
|
46
|
+
addLayer(config) {
|
|
47
|
+
if (this.layers.has(config.key)) {
|
|
48
|
+
this.removeLayer(config.key);
|
|
49
|
+
}
|
|
50
|
+
if (config.zIndex === undefined) {
|
|
51
|
+
config = { ...config, zIndex: -100 + this.layers.size * 10 };
|
|
52
|
+
}
|
|
53
|
+
const layer = new PixiParallaxLayer(this.parent, config, this.viewport);
|
|
54
|
+
this.layers.set(config.key, layer);
|
|
55
|
+
}
|
|
56
|
+
/** Remove a layer by key */
|
|
57
|
+
removeLayer(key) {
|
|
58
|
+
const layer = this.layers.get(key);
|
|
59
|
+
if (layer) {
|
|
60
|
+
layer.destroy();
|
|
61
|
+
this.layers.delete(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Get a layer by key (for direct manipulation) */
|
|
65
|
+
getLayer(key) {
|
|
66
|
+
return this.layers.get(key);
|
|
67
|
+
}
|
|
68
|
+
/** Clean up all layers */
|
|
69
|
+
destroy() {
|
|
70
|
+
for (const layer of this.layers.values()) {
|
|
71
|
+
layer.destroy();
|
|
72
|
+
}
|
|
73
|
+
this.layers.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PixiParallaxLayer } from './PixiParallaxLayer';
|
|
2
|
+
export { PixiParallaxStack } from './PixiParallaxStack';
|
|
3
|
+
export type { PixiLayerConfig, PixiTextureLayerConfig, PixiProceduralLayerConfig } from './PixiParallaxLayer';
|
|
4
|
+
export type { PixiStackConfig } from './PixiParallaxStack';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Mesh } from 'three';
|
|
2
|
+
import type { Scene, Texture } from 'three';
|
|
3
|
+
import type { TextureLayerConfig, ProceduralLayerConfig } from '../core/types';
|
|
4
|
+
export type ThreeLayerConfig = ThreeTextureLayerConfig | ThreeProceduralLayerConfig;
|
|
5
|
+
export interface ThreeTextureLayerConfig extends Omit<TextureLayerConfig, 'texture'> {
|
|
6
|
+
texture: Texture;
|
|
7
|
+
procedural?: never;
|
|
8
|
+
/** Y position in 3D space. Default -1 */
|
|
9
|
+
y?: number;
|
|
10
|
+
/** Z position (depth mode). Further negative = further away. */
|
|
11
|
+
z?: number;
|
|
12
|
+
/** Scale multiplier for depth mode. Default 1 */
|
|
13
|
+
scale?: number;
|
|
14
|
+
/** Texture repeat count. Default 10 */
|
|
15
|
+
textureRepeat?: number;
|
|
16
|
+
/** Opacity. Default 1 */
|
|
17
|
+
opacity?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ThreeProceduralLayerConfig extends ProceduralLayerConfig {
|
|
20
|
+
/** Y position in 3D space. Default -1 */
|
|
21
|
+
y?: number;
|
|
22
|
+
/** Z position (depth mode). */
|
|
23
|
+
z?: number;
|
|
24
|
+
/** Scale multiplier for depth mode. Default 1 */
|
|
25
|
+
scale?: number;
|
|
26
|
+
/** Texture repeat count. Default 10 */
|
|
27
|
+
textureRepeat?: number;
|
|
28
|
+
/** Opacity. Default 1 */
|
|
29
|
+
opacity?: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class ThreeParallaxLayer {
|
|
32
|
+
readonly mesh: Mesh;
|
|
33
|
+
readonly key: string;
|
|
34
|
+
private _speed;
|
|
35
|
+
private material;
|
|
36
|
+
private mode;
|
|
37
|
+
get speed(): number;
|
|
38
|
+
set speed(v: number);
|
|
39
|
+
constructor(scene: Scene, config: ThreeLayerConfig, mode: 'uv-offset' | 'depth', viewport: {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
});
|
|
43
|
+
/** Update layer based on camera position */
|
|
44
|
+
update(camX: number, camZ: number): void;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Mesh, PlaneGeometry, MeshBasicMaterial, RepeatWrapping, DoubleSide, CanvasTexture, } from 'three';
|
|
2
|
+
import { generateGradient, generateStarfield } from '../core/generators';
|
|
3
|
+
export class ThreeParallaxLayer {
|
|
4
|
+
mesh;
|
|
5
|
+
key;
|
|
6
|
+
_speed;
|
|
7
|
+
material;
|
|
8
|
+
mode;
|
|
9
|
+
get speed() {
|
|
10
|
+
return this._speed;
|
|
11
|
+
}
|
|
12
|
+
set speed(v) {
|
|
13
|
+
this._speed = v;
|
|
14
|
+
}
|
|
15
|
+
constructor(scene, config, mode, viewport) {
|
|
16
|
+
this.key = config.key;
|
|
17
|
+
this._speed = config.speed ?? 0.5;
|
|
18
|
+
this.mode = mode;
|
|
19
|
+
const w = config.width ?? viewport.width * 4;
|
|
20
|
+
const h = config.height ?? viewport.height * 4;
|
|
21
|
+
const geo = new PlaneGeometry(w, h);
|
|
22
|
+
let tex;
|
|
23
|
+
if (config.procedural) {
|
|
24
|
+
const proc = config.procedural;
|
|
25
|
+
let canvas;
|
|
26
|
+
if (proc.type === 'gradient') {
|
|
27
|
+
canvas = generateGradient(512, 512, proc);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
canvas = generateStarfield(512, 512, proc);
|
|
31
|
+
}
|
|
32
|
+
tex = new CanvasTexture(canvas);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
tex = config.texture;
|
|
36
|
+
}
|
|
37
|
+
// Configure texture wrapping for repeating
|
|
38
|
+
tex.wrapS = RepeatWrapping;
|
|
39
|
+
tex.wrapT = RepeatWrapping;
|
|
40
|
+
const rep = config.textureRepeat ?? 10;
|
|
41
|
+
tex.repeat.set(rep, rep);
|
|
42
|
+
const opacity = config.opacity ?? 1;
|
|
43
|
+
this.material = new MeshBasicMaterial({
|
|
44
|
+
map: tex,
|
|
45
|
+
side: DoubleSide,
|
|
46
|
+
transparent: opacity < 1,
|
|
47
|
+
opacity,
|
|
48
|
+
});
|
|
49
|
+
this.mesh = new Mesh(geo, this.material);
|
|
50
|
+
if (mode === 'depth') {
|
|
51
|
+
// Depth mode: position mesh at real Z distance, face camera
|
|
52
|
+
this.mesh.position.z = config.z ?? -100;
|
|
53
|
+
this.mesh.position.y = config.y ?? 0;
|
|
54
|
+
const s = config.scale ?? 1;
|
|
55
|
+
this.mesh.scale.set(s, s, 1);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// UV-offset mode: flat horizontal plane
|
|
59
|
+
this.mesh.rotation.x = -Math.PI / 2;
|
|
60
|
+
this.mesh.position.y = config.y ?? -1;
|
|
61
|
+
}
|
|
62
|
+
scene.add(this.mesh);
|
|
63
|
+
}
|
|
64
|
+
/** Update layer based on camera position */
|
|
65
|
+
update(camX, camZ) {
|
|
66
|
+
if (this.mode === 'uv-offset') {
|
|
67
|
+
// Scroll texture UV
|
|
68
|
+
if (this.material.map) {
|
|
69
|
+
this.material.map.offset.x = camX * this._speed * 0.001;
|
|
70
|
+
this.material.map.offset.y = camZ * this._speed * 0.001;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Depth mode: perspective projection handles parallax naturally.
|
|
74
|
+
// Optionally follow camera X to keep layer centered.
|
|
75
|
+
if (this.mode === 'depth') {
|
|
76
|
+
this.mesh.position.x = camX;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
destroy() {
|
|
80
|
+
this.mesh.removeFromParent();
|
|
81
|
+
this.mesh.geometry.dispose();
|
|
82
|
+
this.material.dispose();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Scene, Camera } from 'three';
|
|
2
|
+
import type { ParallaxStack, StackConfig } from '../core/types';
|
|
3
|
+
import { ThreeParallaxLayer } from './ThreeParallaxLayer';
|
|
4
|
+
import type { ThreeLayerConfig } from './ThreeParallaxLayer';
|
|
5
|
+
export interface ThreeStackConfig extends StackConfig<ThreeLayerConfig> {
|
|
6
|
+
/** 'uv-offset' = flat planes with texture scrolling (good for top-down).
|
|
7
|
+
* 'depth' = meshes at real Z distances, perspective does the parallax (good for 2.5D side-scrollers).
|
|
8
|
+
* Default 'uv-offset'. */
|
|
9
|
+
mode?: 'uv-offset' | 'depth';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Multi-layer parallax manager for Three.js.
|
|
13
|
+
*
|
|
14
|
+
* Supports two modes:
|
|
15
|
+
* - **uv-offset**: Flat horizontal planes with texture UV scrolling (current @joydle/camera behavior, improved)
|
|
16
|
+
* - **depth**: Meshes placed at real Z distances — perspective projection creates natural parallax
|
|
17
|
+
*/
|
|
18
|
+
export declare class ThreeParallaxStack implements ParallaxStack {
|
|
19
|
+
private scene;
|
|
20
|
+
private layers;
|
|
21
|
+
private mode;
|
|
22
|
+
private viewport;
|
|
23
|
+
private config;
|
|
24
|
+
constructor(scene: Scene, config: ThreeStackConfig);
|
|
25
|
+
/**
|
|
26
|
+
* Update all layers.
|
|
27
|
+
* - For uv-offset mode: pass camera X and Z world positions.
|
|
28
|
+
* - For depth mode: pass camera X and Z (layers follow camera horizontally).
|
|
29
|
+
* - Alternatively pass a Camera object directly.
|
|
30
|
+
*/
|
|
31
|
+
update(camXOrCamera: number | Camera, camZ?: number): void;
|
|
32
|
+
/** Change a layer's scroll speed at runtime */
|
|
33
|
+
setSpeed(key: string, speed: number): void;
|
|
34
|
+
/** Add a new layer at runtime */
|
|
35
|
+
addLayer(config: ThreeLayerConfig): void;
|
|
36
|
+
/** Remove a layer by key */
|
|
37
|
+
removeLayer(key: string): void;
|
|
38
|
+
/** Get a layer by key */
|
|
39
|
+
getLayer(key: string): ThreeParallaxLayer | undefined;
|
|
40
|
+
/** Clean up all layers and resources */
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { distributeSpeed } from '../core/types';
|
|
2
|
+
import { ThreeParallaxLayer } from './ThreeParallaxLayer';
|
|
3
|
+
/**
|
|
4
|
+
* Multi-layer parallax manager for Three.js.
|
|
5
|
+
*
|
|
6
|
+
* Supports two modes:
|
|
7
|
+
* - **uv-offset**: Flat horizontal planes with texture UV scrolling (current @joydle/camera behavior, improved)
|
|
8
|
+
* - **depth**: Meshes placed at real Z distances — perspective projection creates natural parallax
|
|
9
|
+
*/
|
|
10
|
+
export class ThreeParallaxStack {
|
|
11
|
+
scene;
|
|
12
|
+
layers = new Map();
|
|
13
|
+
mode;
|
|
14
|
+
viewport;
|
|
15
|
+
config;
|
|
16
|
+
constructor(scene, config) {
|
|
17
|
+
this.scene = scene;
|
|
18
|
+
this.config = config;
|
|
19
|
+
this.mode = config.mode ?? 'uv-offset';
|
|
20
|
+
this.viewport = config.viewport ?? { width: 1920, height: 1080 };
|
|
21
|
+
const total = config.layers.length;
|
|
22
|
+
for (let i = 0; i < total; i++) {
|
|
23
|
+
const layerCfg = { ...config.layers[i] };
|
|
24
|
+
if (config.autoSpeed && layerCfg.speed === undefined) {
|
|
25
|
+
layerCfg.speed = distributeSpeed(i, total);
|
|
26
|
+
}
|
|
27
|
+
const layer = new ThreeParallaxLayer(scene, layerCfg, this.mode, this.viewport);
|
|
28
|
+
this.layers.set(layerCfg.key, layer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Update all layers.
|
|
33
|
+
* - For uv-offset mode: pass camera X and Z world positions.
|
|
34
|
+
* - For depth mode: pass camera X and Z (layers follow camera horizontally).
|
|
35
|
+
* - Alternatively pass a Camera object directly.
|
|
36
|
+
*/
|
|
37
|
+
update(camXOrCamera, camZ) {
|
|
38
|
+
let cx;
|
|
39
|
+
let cz;
|
|
40
|
+
if (typeof camXOrCamera === 'number') {
|
|
41
|
+
cx = camXOrCamera;
|
|
42
|
+
cz = camZ ?? 0;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
cx = camXOrCamera.position.x;
|
|
46
|
+
cz = camXOrCamera.position.z;
|
|
47
|
+
}
|
|
48
|
+
for (const layer of this.layers.values()) {
|
|
49
|
+
layer.update(cx, cz);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Change a layer's scroll speed at runtime */
|
|
53
|
+
setSpeed(key, speed) {
|
|
54
|
+
const layer = this.layers.get(key);
|
|
55
|
+
if (layer)
|
|
56
|
+
layer.speed = speed;
|
|
57
|
+
}
|
|
58
|
+
/** Add a new layer at runtime */
|
|
59
|
+
addLayer(config) {
|
|
60
|
+
if (this.layers.has(config.key)) {
|
|
61
|
+
this.removeLayer(config.key);
|
|
62
|
+
}
|
|
63
|
+
const layer = new ThreeParallaxLayer(this.scene, config, this.mode, this.viewport);
|
|
64
|
+
this.layers.set(config.key, layer);
|
|
65
|
+
}
|
|
66
|
+
/** Remove a layer by key */
|
|
67
|
+
removeLayer(key) {
|
|
68
|
+
const layer = this.layers.get(key);
|
|
69
|
+
if (layer) {
|
|
70
|
+
layer.destroy();
|
|
71
|
+
this.layers.delete(key);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Get a layer by key */
|
|
75
|
+
getLayer(key) {
|
|
76
|
+
return this.layers.get(key);
|
|
77
|
+
}
|
|
78
|
+
/** Clean up all layers and resources */
|
|
79
|
+
destroy() {
|
|
80
|
+
for (const layer of this.layers.values()) {
|
|
81
|
+
layer.destroy();
|
|
82
|
+
}
|
|
83
|
+
this.layers.clear();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ThreeParallaxLayer } from './ThreeParallaxLayer';
|
|
2
|
+
export { ThreeParallaxStack } from './ThreeParallaxStack';
|
|
3
|
+
export type { ThreeLayerConfig, ThreeTextureLayerConfig, ThreeProceduralLayerConfig } from './ThreeParallaxLayer';
|
|
4
|
+
export type { ThreeStackConfig } from './ThreeParallaxStack';
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joydle/parallax",
|
|
3
|
+
"version": "0.1.20",
|
|
4
|
+
"description": "Multi-layer parallax scrolling — declarative stacks, procedural backgrounds, and depth parallax for PixiJS and Three.js",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./pixi": {
|
|
14
|
+
"types": "./dist/pixi/index.d.ts",
|
|
15
|
+
"import": "./dist/pixi/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./three": {
|
|
18
|
+
"types": "./dist/three/index.d.ts",
|
|
19
|
+
"import": "./dist/three/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc",
|
|
27
|
+
"demo:pixi": "vite serve examples/pixi --config examples/pixi/vite.config.ts",
|
|
28
|
+
"demo:three": "vite serve examples/three --config examples/three/vite.config.ts",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"author": "Joydle <contact@joydle.dev>",
|
|
37
|
+
"homepage": "https://joydle.dev",
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"pixi.js": ">=7.0.0",
|
|
40
|
+
"three": ">=0.150.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"pixi.js": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"three": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"pixi.js": "^8.16.0",
|
|
52
|
+
"three": "^0.170.0",
|
|
53
|
+
"@types/three": "^0.170.0",
|
|
54
|
+
"typescript": "~5.9.3",
|
|
55
|
+
"vite": "^6.0.0",
|
|
56
|
+
"vitest": "^3.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|