mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-02 16:38:14 +00:00
pagescene: animate selection outline with Blender-style orange pulse
Two-color sine pulse between pale orange (0xffa554) and dark orange (0xe87d0d) over a ~3s period, driven by a uTime uniform updated each frame from performance.now(). The motion makes the outline catch the eye regardless of background color — a static white edge was easy to lose against light scene geometry. Splat bbox wireframes share the dark-orange end (no animation) so the two selection cues read in the same visual family. Both surfaces pull from a new selectionColors.ts module to keep the palette in sync.
This commit is contained in:
1 parent
8ed896c512
commit
8fcfc277bb
3 files changed
+57
-12
No files matched your search
@@ -38,17 +38,22 @@
|
||||
// same behavior as three.js' stock OutlinePass and considered acceptable.
|
||||
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
Pass,
|
||||
FullScreenQuad,
|
||||
} from "three/addons/postprocessing/Pass.js";
|
||||
import { Pass, FullScreenQuad } from "three/addons/postprocessing/Pass.js";
|
||||
import { SplatMesh } from "@sparkjsdev/spark";
|
||||
import { MediaFileType } from "../enums";
|
||||
import { findInternalBbox } from "./internalBbox";
|
||||
import {
|
||||
SELECTION_OUTLINE_COLOR,
|
||||
SELECTION_OUTLINE_ACCENT_COLOR,
|
||||
} from "./selectionColors";
|
||||
|
||||
const SELECTION_OUTLINE_COLOR = 0xffffff;
|
||||
// Two-color pulse so the outline reads as motion. Pale-orange ↔ dark-
|
||||
// orange in the Blender editor selection style — both ends sit in a
|
||||
// warm hue that stands out against typical scene neutrals.
|
||||
// Pulse period (seconds). Angular freq baked into the uniform.
|
||||
const SELECTION_PULSE_PERIOD_SEC = 3.0;
|
||||
// Sample radius for the silhouette edge detect, in screen pixels.
|
||||
const OUTLINE_THICKNESS = 1.5;
|
||||
const OUTLINE_THICKNESS = 1;
|
||||
// Depth value at the far plane after clear. Anything below means
|
||||
// "selected geometry rasterized here."
|
||||
const FAR_DEPTH_THRESHOLD = 0.999999;
|
||||
@@ -207,9 +212,11 @@ export class SelectionOutlinePass extends Pass {
|
||||
// Step 3: silhouette overlay quad. Reads selectedTarget.depthTexture,
|
||||
// paints outline color where the 3x3 neighborhood straddles selected/
|
||||
// not-selected, discards otherwise. autoClear=false preserves
|
||||
// writeBuffer from step 1.
|
||||
// writeBuffer from step 1. `uTime` (seconds) drives the color pulse
|
||||
// inside the fragment shader.
|
||||
this.outlineMaterial.uniforms["selectedDepth"].value =
|
||||
this.selectedTarget.depthTexture;
|
||||
this.outlineMaterial.uniforms["uTime"].value = performance.now() * 0.001;
|
||||
const prevAutoClear = renderer.autoClear;
|
||||
renderer.autoClear = false;
|
||||
renderer.setRenderTarget(writeBuffer);
|
||||
@@ -251,6 +258,17 @@ export class SelectionOutlinePass extends Pass {
|
||||
outlineColor: {
|
||||
value: new THREE.Color(SELECTION_OUTLINE_COLOR),
|
||||
},
|
||||
outlineAccentColor: {
|
||||
value: new THREE.Color(SELECTION_OUTLINE_ACCENT_COLOR),
|
||||
},
|
||||
// Angular frequency for the pulse: 2π / period.
|
||||
// Multiply by uTime (seconds) inside the shader.
|
||||
pulseAngularFreq: {
|
||||
value: (2 * Math.PI) / SELECTION_PULSE_PERIOD_SEC,
|
||||
},
|
||||
// Seconds since some arbitrary origin. Updated each render()
|
||||
// from `performance.now() * 0.001`.
|
||||
uTime: { value: 0 },
|
||||
outlineThickness: { value: OUTLINE_THICKNESS },
|
||||
},
|
||||
vertexShader: /* glsl */ `
|
||||
@@ -264,13 +282,22 @@ export class SelectionOutlinePass extends Pass {
|
||||
// selectedDepth.x < ${FAR_DEPTH_THRESHOLD} encodes "selected
|
||||
// geometry rasterized here." Sample a 3x3 neighborhood; if any
|
||||
// neighbor's selected-presence differs from the center's, the
|
||||
// fragment is on the silhouette. Paint outlineColor there;
|
||||
// discard otherwise so the quad blends onto writeBuffer
|
||||
// without overwriting non-silhouette pixels.
|
||||
// fragment is on the silhouette. Non-silhouette fragments
|
||||
// discard so the quad blends onto writeBuffer without
|
||||
// overwriting non-silhouette pixels.
|
||||
//
|
||||
// Silhouette pixels mix between outlineColor and
|
||||
// outlineAccentColor via a 0..1 sine driven by uTime. Two
|
||||
// colors (rather than a single intensity pulse) so the outline
|
||||
// remains visible against backgrounds of either tone — a pure
|
||||
// white pulse is easy to lose against white scene edges.
|
||||
fragmentShader: /* glsl */ `
|
||||
uniform sampler2D selectedDepth;
|
||||
uniform vec4 screenSize;
|
||||
uniform vec3 outlineColor;
|
||||
uniform vec3 outlineAccentColor;
|
||||
uniform float pulseAngularFreq;
|
||||
uniform float uTime;
|
||||
uniform float outlineThickness;
|
||||
|
||||
varying vec2 vUv;
|
||||
@@ -295,7 +322,9 @@ export class SelectionOutlinePass extends Pass {
|
||||
}
|
||||
|
||||
if (!edge) discard;
|
||||
gl_FragColor = vec4(outlineColor, 1.0);
|
||||
float pulse = sin(uTime * pulseAngularFreq) * 0.5 + 0.5;
|
||||
vec3 col = mix(outlineColor, outlineAccentColor, pulse);
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// don't write the literal string inline anywhere.
|
||||
|
||||
import * as THREE from "three";
|
||||
import { SELECTION_OUTLINE_ACCENT_COLOR } from "./selectionColors";
|
||||
|
||||
export const BBOX_INTERNAL_KEY = "__bbox_internal";
|
||||
|
||||
@@ -65,7 +66,10 @@ function createInternalBbox(localBox: THREE.Box3): THREE.LineSegments {
|
||||
boxGeo.dispose();
|
||||
const lines = new THREE.LineSegments(
|
||||
edges,
|
||||
new THREE.LineBasicMaterial({ color: 0xffffff }),
|
||||
// Static dark orange — matches the outline pulse's trough color so
|
||||
// the splat selection cue reads in the same visual family as the
|
||||
// mesh-selection halo. No animation on the bbox itself.
|
||||
new THREE.LineBasicMaterial({ color: SELECTION_OUTLINE_ACCENT_COLOR }),
|
||||
);
|
||||
lines.position.copy(center);
|
||||
lines.userData[BBOX_INTERNAL_KEY] = true;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// Brand selection palette for the editor's 3D scene. Both the outline
|
||||
// pulse in SelectionOutlinePass and the splat bbox wireframe in
|
||||
// internalBbox source their colors from here so the visual language
|
||||
// stays consistent across the two cues. Tweak one constant and both
|
||||
// surfaces follow.
|
||||
|
||||
// Pale orange — the bright end of the outline pulse.
|
||||
export const SELECTION_OUTLINE_COLOR = 0xffa554;
|
||||
|
||||
// Dark orange — the dim end of the outline pulse AND the static
|
||||
// color used for the splat bbox wireframe (no animation there).
|
||||
export const SELECTION_OUTLINE_ACCENT_COLOR = 0xe87d0d;
|
||||
Reference in new issue
Block a user