Skip to content

Commit 1e04091

Browse files
authored
Merge pull request #17 from web3dev1337/feature/readme-refresh
fix: multi-layer animation, 3D preview colors, effect thumbnails
2 parents 54c5973 + 5a4b7fc commit 1e04091

5 files changed

Lines changed: 240 additions & 37 deletions

File tree

src/app.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class App {
8787
}
8888

8989
_initGraphics() {
90-
this.renderer = new THREE.WebGLRenderer();
90+
this.renderer = new THREE.WebGLRenderer({ preserveDrawingBuffer: true });
9191
this.renderer.setSize(512, 512);
9292
if (this.renderer.capabilities.isWebGL2) console.log("[fxgen] WebGL2");
9393

@@ -199,6 +199,7 @@ class App {
199199
_initLayerPanel() {
200200
this.layerPanel = new LayerPanel(this.layerManager, () => this._onLayerChange());
201201
this.layerPanel.build();
202+
this.layerPanel.setRenderer(this.renderer);
202203
}
203204

204205
_onLayerChange() {
@@ -499,9 +500,14 @@ class App {
499500
// --- Core loop ---
500501

501502
animate() {
502-
const isAnimating = this.effectController.animate;
503-
if (isAnimating) {
504-
this.effectController.time += this.clock.getDelta();
503+
// Check if ANY layer is animating, not just the active one
504+
let anyAnimating = false;
505+
const dt = this.clock.getDelta();
506+
for (const layer of this.layerManager.layers) {
507+
if (layer.effectController.animate) {
508+
layer.effectController.time += dt;
509+
anyAnimating = true;
510+
}
505511
}
506512

507513
const timeStr = this.effectController.time.toString() + "0000000";
@@ -510,7 +516,7 @@ class App {
510516

511517
requestAnimationFrame(() => this.animate());
512518

513-
if (isAnimating) {
519+
if (anyAnimating) {
514520
this.render();
515521
return;
516522
}
@@ -689,7 +695,8 @@ class App {
689695

690696
requestRender() {
691697
this.needsRender = true;
692-
if (this.effectController.animate || this.liveRender) {
698+
const anyAnimating = this.layerManager.layers.some(l => l.effectController.animate);
699+
if (anyAnimating || this.liveRender) {
693700
this.render();
694701
}
695702
}

src/defaults.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const EFFECT_TYPES = [
1818

1919
export function getDefaultEffectController() {
2020
return {
21-
animate: false,
21+
animate: true,
2222
time: 0,
2323
resolution: "512",
2424
polarConversion: false,

src/preview-3d.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,11 @@ export class Preview3D {
159159

160160
this.material = new THREE.MeshStandardMaterial({
161161
color: 0xffffff,
162-
roughness: 0.25,
163-
metalness: 0.5,
164-
envMapIntensity: 2.5,
165-
emissive: new THREE.Color(0x1a0628),
166-
emissiveIntensity: 0.8,
162+
roughness: 0.55,
163+
metalness: 0.0,
164+
envMapIntensity: 0.6,
165+
emissive: new THREE.Color(0xffffff),
166+
emissiveIntensity: 0.85,
167167
side: THREE.DoubleSide
168168
});
169169
this.material.normalScale.set(1.5, 1.5);

src/ui/effect-thumbnails.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Generates tiny preview thumbnails for effect types.
3+
* Uses a single shared mini-pipeline to render each effect at low res,
4+
* then caches the result as a data URL. Renders in batches to avoid freezing.
5+
*/
6+
import * as THREE from "three";
7+
import { FxgenShader, FxgenShaderUtils } from "../pixy-api.js";
8+
import { getDefaultEffectController, getDefaultEffectParameters } from "../defaults.js";
9+
import { isCustomEffect, getCustomShaderKey } from "../custom-shader-registry.js";
10+
import { SHADERS } from "../../shader-defs.js";
11+
12+
const THUMB_SIZE = 48;
13+
const BATCH_SIZE = 4;
14+
const cache = new Map(); // effectType → dataURL
15+
16+
let renderer = null;
17+
let scene = null;
18+
let camera = null;
19+
let rt = null;
20+
let quad = null;
21+
let readBuf = null;
22+
let thumbCanvas = null;
23+
let thumbCtx = null;
24+
25+
function ensureSetup(sourceRenderer) {
26+
if (renderer) return;
27+
renderer = sourceRenderer;
28+
scene = new THREE.Scene();
29+
camera = new THREE.Camera();
30+
quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), new THREE.MeshBasicMaterial());
31+
scene.add(quad);
32+
rt = new THREE.WebGLRenderTarget(THUMB_SIZE, THUMB_SIZE, {
33+
minFilter: THREE.LinearFilter,
34+
magFilter: THREE.LinearFilter,
35+
stencilBuffer: false
36+
});
37+
readBuf = new Uint8Array(THUMB_SIZE * THUMB_SIZE * 4);
38+
thumbCanvas = document.createElement("canvas");
39+
thumbCanvas.width = THUMB_SIZE;
40+
thumbCanvas.height = THUMB_SIZE;
41+
thumbCtx = thumbCanvas.getContext("2d");
42+
}
43+
44+
function renderOne(effectType) {
45+
if (cache.has(effectType)) return cache.get(effectType);
46+
47+
const prevRT = renderer.getRenderTarget();
48+
try {
49+
if (isCustomEffect(effectType)) {
50+
const key = getCustomShaderKey(effectType);
51+
const src = key && SHADERS[key];
52+
if (!src) { renderer.setRenderTarget(prevRT); return null; }
53+
const mat = new THREE.ShaderMaterial({
54+
uniforms: {
55+
uTime: { value: 1.5 },
56+
uResolution: { value: new THREE.Vector2(THUMB_SIZE, THUMB_SIZE) },
57+
uMouse: { value: new THREE.Vector2(0.5, 0.5) },
58+
},
59+
vertexShader: `varying vec2 vUv; void main(){ vUv=uv; gl_Position=vec4(position,1.0); }`,
60+
fragmentShader: src,
61+
depthTest: false, depthWrite: false,
62+
});
63+
quad.material = mat;
64+
renderer.setRenderTarget(rt);
65+
renderer.render(scene, camera);
66+
renderer.setRenderTarget(prevRT);
67+
mat.dispose();
68+
} else {
69+
const shader = new FxgenShader();
70+
shader.enable(effectType.toUpperCase());
71+
shader.enable("TOON");
72+
shader.enable("GLSL3");
73+
const uniforms = shader.generateUniforms();
74+
const mat = shader.createMaterial(uniforms);
75+
mat.defines = shader.generateDefines();
76+
77+
const ec = Object.assign(getDefaultEffectController(), getDefaultEffectParameters());
78+
ec.type = effectType;
79+
ec.time = 1.5;
80+
81+
for (const key in ec) {
82+
if (uniforms[key] !== undefined) {
83+
FxgenShaderUtils.SetShaderParameter(uniforms, key, ec[key]);
84+
}
85+
}
86+
FxgenShaderUtils.SetShaderParameter(uniforms, "resolution", new THREE.Vector2(THUMB_SIZE, THUMB_SIZE));
87+
if (uniforms.time) uniforms.time.value = 1.5;
88+
89+
quad.material = mat;
90+
renderer.setRenderTarget(rt);
91+
renderer.render(scene, camera);
92+
renderer.setRenderTarget(prevRT);
93+
mat.dispose();
94+
}
95+
96+
// Read pixels and draw to canvas
97+
renderer.readRenderTargetPixels(rt, 0, 0, THUMB_SIZE, THUMB_SIZE, readBuf);
98+
const imgData = thumbCtx.createImageData(THUMB_SIZE, THUMB_SIZE);
99+
// WebGL reads bottom-up, flip vertically
100+
for (let y = 0; y < THUMB_SIZE; y++) {
101+
const srcRow = (THUMB_SIZE - 1 - y) * THUMB_SIZE * 4;
102+
const dstRow = y * THUMB_SIZE * 4;
103+
for (let x = 0; x < THUMB_SIZE * 4; x++) {
104+
imgData.data[dstRow + x] = readBuf[srcRow + x];
105+
}
106+
}
107+
thumbCtx.putImageData(imgData, 0, 0);
108+
const url = thumbCanvas.toDataURL("image/jpeg", 0.7);
109+
cache.set(effectType, url);
110+
return url;
111+
} catch (e) {
112+
renderer.setRenderTarget(prevRT);
113+
cache.set(effectType, null);
114+
return null;
115+
}
116+
}
117+
118+
/**
119+
* Generate thumbnails for a list of effect types in batches.
120+
* Calls onThumb(effectType, dataURL) for each completed thumbnail.
121+
* Returns a cancel function.
122+
*/
123+
export function generateThumbnails(sourceRenderer, effectTypes, onThumb) {
124+
ensureSetup(sourceRenderer);
125+
let idx = 0;
126+
let cancelled = false;
127+
128+
function processBatch() {
129+
if (cancelled || idx >= effectTypes.length) return;
130+
const end = Math.min(idx + BATCH_SIZE, effectTypes.length);
131+
for (let i = idx; i < end; i++) {
132+
const fx = effectTypes[i];
133+
if (cache.has(fx)) {
134+
onThumb(fx, cache.get(fx));
135+
} else {
136+
const url = renderOne(fx);
137+
onThumb(fx, url);
138+
}
139+
}
140+
idx = end;
141+
if (idx < effectTypes.length) {
142+
requestAnimationFrame(processBatch);
143+
}
144+
}
145+
146+
requestAnimationFrame(processBatch);
147+
return () => { cancelled = true; };
148+
}
149+
150+
export function getCachedThumbnail(effectType) {
151+
return cache.get(effectType) || null;
152+
}

0 commit comments

Comments
 (0)