chore(website): rename twenty-website-new → twenty-website (#20745)

## Summary
Follow-up to the Cloudflare/OpenNext migration (#20741). Now that the
legacy `twenty-website` package was already removed in #20270, the
`-new` suffix on the marketing site package is no longer meaningful.

## What changes
- **Directory rename**: `git mv packages/twenty-website-new
packages/twenty-website` (1213 files moved, no content change)
- **Package + nx config**: `package.json` and `project.json` name fields
updated, `sourceRoot` repointed
- **Source refs**: `load-local-articles.ts` and
`load-local-release-notes.ts` had a hardcoded `'twenty-website-new'`
segment in their monorepo-root fallback path;
`app/[locale]/releases/page.tsx` had display strings showing where to
add content
- **External refs**: root `package.json` workspaces, root `CLAUDE.md` /
`README.md`, `twenty-sdk` + `create-twenty-app` READMEs,
`.vscode/twenty.code-workspace`, `.cursor/rules/changelog-process.mdc`,
Crowdin config + the three `website-i18n-*` CI workflows +
`ci-website.yaml`
- **Docker cleanup**:
`packages/twenty-docker/twenty-website-new/Dockerfile` deleted; the two
Makefile targets (`prod-website-new-build` / `prod-website-new-run`)
that referenced it removed — EKS deploy was retired in the Cloudflare
migration
- **`yarn.lock`** regenerated against the new workspace path

## What's deliberately not in this PR
The dev hostname `website-new.twenty-main.com` in `wrangler.jsonc` stays
for now. Migrating it to `website.twenty-main.com` needs coordinated DNS
deletion (current CNAME points at the legacy Docusaurus NLB and serves
503s) and removal of the matching legacy `website` Helm chart in
`twenty-infra`. Flagged as a separate cleanup.

Companion infra PR: https://github.com/twentyhq/twenty-infra/pull/682
(workflow paths + Terraform ECR + docs)

## Test plan
- [x] `yarn install --immutable` resolves clean against the new path
- [x] `npx nx typecheck twenty-website` passes
- [x] `npx nx lint twenty-website` passes
- [ ] CI on this PR confirms the same on a fresh checkout
- [ ] After merge: trigger `Deploy Website` workflow against
`environment=dev` to confirm the renamed working-directory deploys
correctly
This commit is contained in:
Félix Malfait
2026-05-19 23:42:09 +02:00
committed by GitHub
parent 24a836cc7d
commit 658bdf3e57
1228 changed files with 106 additions and 151 deletions
@@ -0,0 +1,101 @@
import { Container } from '@/design-system/components';
import {
Heading as BaseHeading,
type HeadingProps,
} from '@/design-system/components/Heading';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
import { Points } from './ProblemPoints';
import { Visual } from './components/Visual/Visual';
const StyledSection = styled.section`
width: 100%;
`;
const StyledContainer = styled(Container)`
display: grid;
grid-template-columns: 1fr;
row-gap: ${theme.spacing(12)};
padding-bottom: ${theme.spacing(12)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(6)};
@media (min-width: ${theme.breakpoints.md}px) {
column-gap: ${theme.spacing(4)};
grid-template-columns: 1fr 1fr;
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
padding-top: ${theme.spacing(6)};
padding-bottom: ${theme.spacing(10)};
row-gap: ${theme.spacing(20)};
}
`;
const StyledContent = styled.div`
display: grid;
grid-template-columns: 1fr;
row-gap: ${theme.spacing(10)};
@media (min-width: ${theme.breakpoints.md}px) {
padding-bottom: ${theme.spacing(15)};
padding-left: ${theme.spacing(15)};
padding-right: ${theme.spacing(15)};
padding-top: ${theme.spacing(15)};
row-gap: ${theme.spacing(20)};
}
`;
const problemHeadingClassName = css`
margin-top: calc(${theme.spacing(2)} - ${theme.spacing(10)});
@media (min-width: ${theme.breakpoints.md}px) {
margin-top: calc(${theme.spacing(2)} - ${theme.spacing(16)});
}
`;
type RootProps = { children: ReactNode };
function Root({ children }: RootProps) {
return (
<StyledSection>
<StyledContainer>{children}</StyledContainer>
</StyledSection>
);
}
type ContentProps = { children: ReactNode };
function Content({ children }: ContentProps) {
return <StyledContent>{children}</StyledContent>;
}
function Heading({
as = 'h2',
children,
className,
size = 'md',
weight = 'light',
}: HeadingProps & { children?: ReactNode }) {
return (
<BaseHeading
as={as}
className={[problemHeadingClassName, className].filter(Boolean).join(' ')}
size={size}
weight={weight}
>
{children}
</BaseHeading>
);
}
export const Problem = {
Root,
Visual,
Content,
Heading,
Points,
};
@@ -0,0 +1,56 @@
import { Body } from '@/design-system/components/Body';
import { getServerI18n } from '@/lib/i18n/utils/get-server-i18n';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import React from 'react';
import type { ProblemPointType } from './problem-point-type';
const StyledPoints = styled.div`
display: grid;
grid-template-columns: 1fr;
row-gap: ${theme.spacing(6)};
@media (min-width: ${theme.breakpoints.md}px) {
max-width: 454px;
}
`;
const StyledDivider = styled.div`
height: 0;
border-top: 0.75px dashed ${theme.colors.primary.border[80]};
width: 100%;
opacity: 50%;
`;
const StyledPoint = styled.div`
display: grid;
grid-template-columns: 1fr;
row-gap: ${theme.spacing(2)};
@media (min-width: ${theme.breakpoints.md}px) {
max-width: 380px;
width: 100%;
}
`;
type PointsProps = {
points: ProblemPointType[];
};
export function Points({ points }: PointsProps) {
const i18n = getServerI18n();
return (
<StyledPoints>
{points.map((point, index) => (
<React.Fragment key={index}>
<StyledDivider />
<StyledPoint>
{point.heading}
<Body size="sm">{i18n._(point.body)}</Body>
</StyledPoint>
</React.Fragment>
))}
</StyledPoints>
);
}
@@ -0,0 +1,33 @@
'use client';
import { useRef, type CSSProperties } from 'react';
import { DEFAULT_TUNING } from './monolith-halftone-mount';
import { useMonolithHalftone } from './use-monolith-halftone';
const IMAGE_SRC = '/images/home/problem/monolith-problem.webp';
type MonolithProps = {
imageUrl?: string;
style?: CSSProperties;
};
export default function Monolith({
imageUrl = IMAGE_SRC,
style,
}: MonolithProps) {
const mountReference = useRef<HTMLDivElement>(null);
useMonolithHalftone({ imageUrl, mountRef: mountReference });
return (
<div
ref={mountReference}
style={{
background: DEFAULT_TUNING.backgroundColor,
height: '100%',
width: '100%',
...style,
}}
/>
);
}
@@ -0,0 +1,64 @@
import { WebGlMount } from '@/lib/visual-runtime';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import Monolith from './Monolith';
const DESKTOP_PATH =
'M672 395.498V701a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4h664a4 4 0 0 1 4 4v65.155c0 2.363-.837 4.65-2.361 6.454l-23.603 27.936a10 10 0 0 0-2.362 6.453v242.614c0 2.245.756 4.424 2.145 6.188l24.036 30.51a10 10 0 0 1 2.145 6.188';
const MOBILE_PATH =
'M360 214.218v165.689a2.093 2.093 0 0 1-2.093 2.093H2.093A2.093 2.093 0 0 1 0 379.907V2.093C0 .937.937 0 2.093 0h355.814C359.063 0 360 .937 360 2.093v35.463a5.23 5.23 0 0 1-1.217 3.355l-12.741 15.252a5.23 5.23 0 0 0-1.216 3.354v131.624a5.23 5.23 0 0 0 1.104 3.215l12.966 16.646a5.24 5.24 0 0 1 1.104 3.216';
const desktopMask = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 672 705' preserveAspectRatio='none'%3E%3Cpath d='${encodeURIComponent(DESKTOP_PATH)}' fill='black'/%3E%3C/svg%3E")`;
const mobileMask = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 360 382' preserveAspectRatio='none'%3E%3Cpath d='${encodeURIComponent(MOBILE_PATH)}' fill='black'/%3E%3C/svg%3E")`;
const StyledVisual = styled.div`
min-height: 382px;
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
min-height: 0;
}
`;
const StyledMasked = styled.div`
background-color: #1c1c1c;
height: 100%;
isolation: isolate;
-webkit-mask-image: ${mobileMask};
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-image: ${mobileMask};
mask-position: center;
mask-repeat: no-repeat;
mask-size: 100% 100%;
overflow: hidden;
position: relative;
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
-webkit-mask-image: ${desktopMask};
mask-image: ${desktopMask};
}
`;
const StyledHalftoneLayer = styled.div`
inset: 0;
position: absolute;
`;
export function Visual() {
return (
<StyledVisual>
<StyledMasked>
<StyledHalftoneLayer>
<WebGlMount>
<Monolith />
</WebGlMount>
</StyledHalftoneLayer>
</StyledMasked>
</StyledVisual>
);
}
@@ -0,0 +1,819 @@
import * as THREE from 'three';
import { observeElementSize } from '@/lib/dom/observe-element-size';
import {
createVisualRenderLoop,
loadVisualImage,
tryCreateSiteWebGlRenderer,
type VisualRenderLoop,
type VisualRenderLoopFrame,
} from '@/lib/visual-runtime';
const PREVIEW_DISTANCE = 4;
const VIRTUAL_RENDER_HEIGHT = 768;
const REFERENCE_PREVIEW_DISTANCE = 4;
const MIN_FOOTPRINT_SCALE = 0.001;
const IMAGE_POINTER_FOLLOW = 0.38;
const IMAGE_POINTER_VELOCITY_DAMPING = 0.82;
export type HalftoneTuning = {
dashColor: string;
imageContrast: number;
power: number;
scale: number;
width: number;
};
export type AnimationTuning = {
hoverLightEnabled: boolean;
hoverLightIntensity: number;
hoverLightRadius: number;
waveSpeed: number;
};
export type MonolithTuning = {
animation: AnimationTuning;
backgroundColor: string;
halftone: HalftoneTuning;
};
export const DEFAULT_TUNING: MonolithTuning = {
halftone: {
dashColor: '#BABABA',
imageContrast: 1,
power: 0.21,
scale: 10,
width: 0.65,
},
animation: {
hoverLightEnabled: true,
hoverLightIntensity: 0.13,
hoverLightRadius: 1,
waveSpeed: 1,
},
backgroundColor: '#1C1C1C',
};
const passThroughVertexShader = /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`;
const blurFragmentShader = /* glsl */ `
precision highp float;
uniform sampler2D tInput;
uniform vec2 dir;
uniform vec2 res;
varying vec2 vUv;
void main() {
vec4 sum = vec4(0.0);
vec2 px = dir / res;
float w[5];
w[0] = 0.227027;
w[1] = 0.1945946;
w[2] = 0.1216216;
w[3] = 0.054054;
w[4] = 0.016216;
sum += texture2D(tInput, vUv) * w[0];
for (int i = 1; i < 5; i++) {
float fi = float(i) * 3.0;
sum += texture2D(tInput, vUv + px * fi) * w[i];
sum += texture2D(tInput, vUv - px * fi) * w[i];
}
gl_FragColor = sum;
}
`;
const halftoneFragmentShader = /* glsl */ `
precision highp float;
uniform sampler2D tScene;
uniform sampler2D tGlow;
uniform vec2 effectResolution;
uniform vec2 logicalResolution;
uniform float tile;
uniform float s_3;
uniform float s_4;
uniform vec3 dashColor;
uniform float time;
uniform float waveAmount;
uniform float waveSpeed;
uniform float footprintScale;
uniform vec2 interactionUv;
uniform vec2 interactionVelocity;
uniform vec2 dragOffset;
uniform float hoverLightStrength;
uniform float hoverLightRadius;
uniform float hoverFlowStrength;
uniform float hoverFlowRadius;
uniform float dragFlowStrength;
uniform float cropToBounds;
varying vec2 vUv;
float distSegment(in vec2 p, in vec2 a, in vec2 b) {
vec2 pa = p - a;
vec2 ba = b - a;
float denom = max(dot(ba, ba), 0.000001);
float h = clamp(dot(pa, ba) / denom, 0.0, 1.0);
return length(pa - ba * h);
}
float lineSimpleEt(in vec2 p, in float r, in float thickness) {
vec2 a = vec2(0.5) + vec2(-r, 0.0);
vec2 b = vec2(0.5) + vec2(r, 0.0);
float distToSegment = distSegment(p, a, b);
float halfThickness = thickness * r;
return distToSegment - halfThickness;
}
void main() {
if (cropToBounds > 0.5) {
vec4 boundsCheck = texture2D(tScene, vUv);
if (boundsCheck.a < 0.01) {
gl_FragColor = vec4(0.0);
return;
}
}
vec2 fragCoord =
(gl_FragCoord.xy / max(effectResolution, vec2(1.0))) * logicalResolution;
float halftoneSize = max(tile * max(footprintScale, 0.001), 1.0);
vec2 pointerPx = interactionUv * logicalResolution;
vec2 fragDelta = fragCoord - pointerPx;
float fragDist = length(fragDelta);
vec2 radialDir = fragDist > 0.001 ? fragDelta / fragDist : vec2(0.0, 1.0);
float velocityMagnitude = length(interactionVelocity);
vec2 motionDir = velocityMagnitude > 0.001
? interactionVelocity / velocityMagnitude
: vec2(0.0, 0.0);
float motionBias = velocityMagnitude > 0.001
? dot(-radialDir, motionDir) * 0.5 + 0.5
: 0.5;
float hoverLightMask = 0.0;
if (hoverLightStrength > 0.0) {
float lightRadiusPx = hoverLightRadius * logicalResolution.y;
hoverLightMask = smoothstep(lightRadiusPx, 0.0, fragDist);
}
float hoverFlowMask = 0.0;
if (hoverFlowStrength > 0.0) {
float hoverRadiusPx = hoverFlowRadius * logicalResolution.y;
hoverFlowMask = smoothstep(hoverRadiusPx, 0.0, fragDist);
}
vec2 hoverDisplacement =
radialDir * hoverFlowStrength * hoverFlowMask * halftoneSize * 0.55 +
motionDir * hoverFlowStrength * hoverFlowMask *
(0.4 + motionBias) * halftoneSize * 1.15;
vec2 travelDisplacement = dragOffset * dragFlowStrength * 0.45;
vec2 effectCoord = fragCoord + hoverDisplacement + travelDisplacement;
float bandRow = floor(effectCoord.y / halftoneSize);
float waveOffset =
waveAmount * sin(time * waveSpeed + bandRow * 0.5) * halftoneSize;
effectCoord.x += waveOffset;
vec2 cellIndex = floor(effectCoord / halftoneSize);
vec2 sampleUv = clamp(
(cellIndex + 0.5) * halftoneSize / logicalResolution,
vec2(0.0),
vec2(1.0)
);
vec2 cellUv = fract(effectCoord / halftoneSize);
vec4 sceneSample = texture2D(tScene, sampleUv);
float mask = smoothstep(0.02, 0.08, sceneSample.a);
float lightLift =
hoverLightStrength * hoverLightMask * mix(0.78, 1.18, motionBias) * 0.22;
float bandRadius = clamp(
(
(
sceneSample.r +
sceneSample.g +
sceneSample.b +
s_3 * length(vec2(0.5))
) * (1.0 / 3.0)
) + lightLift,
0.0,
1.0
) * 1.86 * 0.5;
float alpha = 0.0;
if (bandRadius > 0.0001) {
float signedDistance = lineSimpleEt(cellUv, bandRadius, s_4);
float edge = 0.02;
alpha = (1.0 - smoothstep(0.0, edge, signedDistance)) * mask;
}
vec3 color = dashColor * alpha;
gl_FragColor = vec4(color, alpha);
#include <tonemapping_fragment>
#include <colorspace_fragment>
}
`;
const imagePassthroughFragmentShader = /* glsl */ `
precision highp float;
uniform sampler2D tImage;
uniform vec2 imageSize;
uniform vec2 viewportSize;
uniform float zoom;
uniform float contrast;
varying vec2 vUv;
void main() {
float imageAspect = imageSize.x / imageSize.y;
float viewAspect = viewportSize.x / viewportSize.y;
vec2 uv = vUv;
// Cover: fill the full masked area and crop overflow.
if (imageAspect > viewAspect) {
float scale = viewAspect / imageAspect;
uv.x = (uv.x - 0.5) * scale + 0.5;
} else {
float scale = imageAspect / viewAspect;
uv.y = (uv.y - 0.5) * scale + 0.5;
}
uv = (uv - 0.5) / zoom + 0.5;
float inBounds = step(0.0, uv.x) * step(uv.x, 1.0)
* step(0.0, uv.y) * step(uv.y, 1.0);
vec4 color = texture2D(tImage, clamp(uv, 0.0, 1.0));
vec3 contrastColor = clamp((color.rgb - 0.5) * contrast + 0.5, 0.0, 1.0);
gl_FragColor = vec4(contrastColor, inBounds);
}
`;
type Rect = {
height: number;
width: number;
x: number;
y: number;
};
type InteractionState = {
mouseX: number;
mouseY: number;
pointerInside: boolean;
pointerVelocityX: number;
pointerVelocityY: number;
smoothedMouseX: number;
smoothedMouseY: number;
};
export type MountHalftoneCanvasOptions = {
container: HTMLDivElement;
imageUrl: string;
signal: AbortSignal;
};
function clampRectToViewport(
rect: Rect,
viewportWidth: number,
viewportHeight: number,
) {
const minX = Math.max(rect.x, 0);
const minY = Math.max(rect.y, 0);
const maxX = Math.min(rect.x + rect.width, viewportWidth);
const maxY = Math.min(rect.y + rect.height, viewportHeight);
if (maxX <= minX || maxY <= minY) {
return null;
}
return {
x: minX,
y: minY,
width: maxX - minX,
height: maxY - minY,
};
}
function getRectArea(rect: Rect | null) {
if (!rect) {
return 0;
}
return Math.max(rect.width, 0) * Math.max(rect.height, 0);
}
function createRenderTarget(width: number, height: number) {
return new THREE.WebGLRenderTarget(width, height, {
format: THREE.RGBAFormat,
magFilter: THREE.LinearFilter,
minFilter: THREE.LinearFilter,
});
}
function createInteractionState(): InteractionState {
return {
mouseX: 0.5,
mouseY: 0.5,
pointerInside: false,
pointerVelocityX: 0,
pointerVelocityY: 0,
smoothedMouseX: 0.5,
smoothedMouseY: 0.5,
};
}
function getImagePreviewZoom(previewDistance: number) {
return REFERENCE_PREVIEW_DISTANCE / Math.max(previewDistance, 0.001);
}
function getContainedImageRect(options: {
imageHeight: number;
imageWidth: number;
viewportHeight: number;
viewportWidth: number;
zoom: number;
}) {
const { imageHeight, imageWidth, viewportHeight, viewportWidth, zoom } =
options;
if (
imageWidth <= 0 ||
imageHeight <= 0 ||
viewportWidth <= 0 ||
viewportHeight <= 0
) {
return null;
}
const imageAspect = imageWidth / imageHeight;
const viewAspect = viewportWidth / viewportHeight;
let fittedWidth = viewportWidth;
let fittedHeight = viewportHeight;
if (imageAspect > viewAspect) {
fittedHeight = viewportWidth / imageAspect;
} else {
fittedWidth = viewportHeight * imageAspect;
}
const scaledWidth = fittedWidth * zoom;
const scaledHeight = fittedHeight * zoom;
return clampRectToViewport(
{
x: (viewportWidth - scaledWidth) * 0.5,
y: (viewportHeight - scaledHeight) * 0.5,
width: scaledWidth,
height: scaledHeight,
},
viewportWidth,
viewportHeight,
);
}
function getFootprintScaleFromRects(
currentRect: Rect | null,
referenceRect: Rect | null,
) {
const currentArea = getRectArea(currentRect);
const referenceArea = getRectArea(referenceRect);
if (currentArea <= 0 || referenceArea <= 0) {
return 1;
}
return Math.max(Math.sqrt(currentArea / referenceArea), MIN_FOOTPRINT_SCALE);
}
function getImageFootprintScale(options: {
imageHeight: number;
imageWidth: number;
previewDistance: number;
viewportHeight: number;
viewportWidth: number;
}) {
const {
imageHeight,
imageWidth,
previewDistance,
viewportHeight,
viewportWidth,
} = options;
const currentRect = getContainedImageRect({
imageHeight,
imageWidth,
viewportHeight,
viewportWidth,
zoom: getImagePreviewZoom(previewDistance),
});
const referenceRect = getContainedImageRect({
imageHeight,
imageWidth,
viewportHeight,
viewportWidth,
zoom: 1,
});
return getFootprintScaleFromRects(currentRect, referenceRect);
}
function createAbortError() {
return new DOMException('Aborted', 'AbortError');
}
function createAbortTask(signal: AbortSignal) {
let cleanup = () => {};
const promise = new Promise<never>((_, reject) => {
if (signal.aborted) {
reject(createAbortError());
return;
}
const handleAbort = () => {
reject(createAbortError());
};
signal.addEventListener('abort', handleAbort, { once: true });
cleanup = () => {
signal.removeEventListener('abort', handleAbort);
};
});
return { cleanup, promise };
}
export async function mountHalftoneCanvas(options: MountHalftoneCanvasOptions) {
const { container, imageUrl, signal } = options;
const tuning = DEFAULT_TUNING;
const getWidth = () => Math.max(container.clientWidth, 1);
const getHeight = () => Math.max(container.clientHeight, 1);
const getVirtualHeight = () => Math.max(VIRTUAL_RENDER_HEIGHT, getHeight());
const getVirtualWidth = () =>
Math.max(
Math.round(getVirtualHeight() * (getWidth() / Math.max(getHeight(), 1))),
1,
);
const abortTask = createAbortTask(signal);
const image = await Promise.race([
loadVisualImage(imageUrl, { label: 'problem monolith image' }),
abortTask.promise,
]).finally(abortTask.cleanup);
if (signal.aborted) {
return undefined;
}
let renderLoop: VisualRenderLoop | null = null;
const renderer = tryCreateSiteWebGlRenderer({
alpha: true,
antialias: false,
onContextLost: () => {
renderLoop?.stop();
},
});
if (renderer === null) {
return undefined;
}
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(1);
renderer.setSize(getVirtualWidth(), getVirtualHeight(), false);
const canvas = renderer.domElement;
canvas.style.cursor = 'default';
canvas.style.display = 'block';
canvas.style.height = '100%';
canvas.style.touchAction = 'none';
canvas.style.width = '100%';
container.appendChild(canvas);
const imageTexture = new THREE.Texture(image);
imageTexture.colorSpace = THREE.SRGBColorSpace;
imageTexture.needsUpdate = true;
const sceneTarget = createRenderTarget(getVirtualWidth(), getVirtualHeight());
const blurTargetA = createRenderTarget(getVirtualWidth(), getVirtualHeight());
const blurTargetB = createRenderTarget(getVirtualWidth(), getVirtualHeight());
const fullScreenGeometry = new THREE.PlaneGeometry(2, 2);
const orthographicCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
const imageMaterial = new THREE.ShaderMaterial({
uniforms: {
contrast: { value: tuning.halftone.imageContrast },
imageSize: { value: new THREE.Vector2(image.width, image.height) },
tImage: { value: imageTexture },
viewportSize: {
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
},
zoom: { value: getImagePreviewZoom(PREVIEW_DISTANCE) },
},
vertexShader: passThroughVertexShader,
fragmentShader: imagePassthroughFragmentShader,
});
const imageScene = new THREE.Scene();
imageScene.add(new THREE.Mesh(fullScreenGeometry, imageMaterial));
const blurHorizontalMaterial = new THREE.ShaderMaterial({
uniforms: {
dir: { value: new THREE.Vector2(1, 0) },
res: { value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()) },
tInput: { value: null },
},
vertexShader: passThroughVertexShader,
fragmentShader: blurFragmentShader,
});
const blurVerticalMaterial = new THREE.ShaderMaterial({
uniforms: {
dir: { value: new THREE.Vector2(0, 1) },
res: { value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()) },
tInput: { value: null },
},
vertexShader: passThroughVertexShader,
fragmentShader: blurFragmentShader,
});
const halftoneMaterial = new THREE.ShaderMaterial({
transparent: true,
uniforms: {
cropToBounds: { value: 1 },
dashColor: { value: new THREE.Color(tuning.halftone.dashColor) },
dragFlowStrength: { value: 0 },
dragOffset: { value: new THREE.Vector2(0, 0) },
effectResolution: {
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
},
footprintScale: { value: 1 },
hoverFlowRadius: { value: 0.18 },
hoverFlowStrength: { value: 0 },
hoverLightRadius: { value: tuning.animation.hoverLightRadius },
hoverLightStrength: { value: 0 },
interactionUv: { value: new THREE.Vector2(0.5, 0.5) },
interactionVelocity: { value: new THREE.Vector2(0, 0) },
logicalResolution: {
value: new THREE.Vector2(getVirtualWidth(), getVirtualHeight()),
},
s_3: { value: tuning.halftone.power },
s_4: { value: tuning.halftone.width },
tGlow: { value: blurTargetB.texture },
tile: { value: tuning.halftone.scale },
time: { value: 0 },
tScene: { value: sceneTarget.texture },
waveAmount: { value: 0 },
waveSpeed: { value: tuning.animation.waveSpeed },
},
vertexShader: passThroughVertexShader,
fragmentShader: halftoneFragmentShader,
});
const blurHorizontalScene = new THREE.Scene();
blurHorizontalScene.add(
new THREE.Mesh(fullScreenGeometry, blurHorizontalMaterial),
);
const blurVerticalScene = new THREE.Scene();
blurVerticalScene.add(
new THREE.Mesh(fullScreenGeometry, blurVerticalMaterial),
);
const postScene = new THREE.Scene();
postScene.add(new THREE.Mesh(fullScreenGeometry, halftoneMaterial));
const updateViewportUniforms = (
logicalWidth: number,
logicalHeight: number,
effectWidth: number,
effectHeight: number,
) => {
blurHorizontalMaterial.uniforms.res.value.set(effectWidth, effectHeight);
blurVerticalMaterial.uniforms.res.value.set(effectWidth, effectHeight);
halftoneMaterial.uniforms.effectResolution.value.set(
effectWidth,
effectHeight,
);
halftoneMaterial.uniforms.logicalResolution.value.set(
logicalWidth,
logicalHeight,
);
imageMaterial.uniforms.viewportSize.value.set(logicalWidth, logicalHeight);
};
const getHalftoneScale = () =>
getImageFootprintScale({
imageHeight: image.height,
imageWidth: image.width,
previewDistance: PREVIEW_DISTANCE,
viewportHeight: getVirtualHeight(),
viewportWidth: getVirtualWidth(),
});
const interaction = createInteractionState();
const syncSize = () => {
const virtualWidth = getVirtualWidth();
const virtualHeight = getVirtualHeight();
renderer.setSize(virtualWidth, virtualHeight, false);
sceneTarget.setSize(virtualWidth, virtualHeight);
blurTargetA.setSize(virtualWidth, virtualHeight);
blurTargetB.setSize(virtualWidth, virtualHeight);
updateViewportUniforms(
virtualWidth,
virtualHeight,
virtualWidth,
virtualHeight,
);
};
const stopObservingSize = observeElementSize(container, syncSize);
const updatePointerPosition = (
event: PointerEvent,
options?: { resetVelocity?: boolean },
) => {
const rect = canvas.getBoundingClientRect();
const width = Math.max(rect.width, 1);
const height = Math.max(rect.height, 1);
const nextMouseX = THREE.MathUtils.clamp(
(event.clientX - rect.left) / width,
0,
1,
);
const nextMouseY = THREE.MathUtils.clamp(
(event.clientY - rect.top) / height,
0,
1,
);
const deltaX = nextMouseX - interaction.mouseX;
const deltaY = nextMouseY - interaction.mouseY;
interaction.mouseX = nextMouseX;
interaction.mouseY = nextMouseY;
interaction.pointerInside =
event.clientX >= rect.left &&
event.clientX <= rect.right &&
event.clientY >= rect.top &&
event.clientY <= rect.bottom;
if (options?.resetVelocity) {
interaction.pointerVelocityX = 0;
interaction.pointerVelocityY = 0;
interaction.smoothedMouseX = nextMouseX;
interaction.smoothedMouseY = nextMouseY;
return;
}
interaction.pointerVelocityX = deltaX;
interaction.pointerVelocityY = deltaY;
};
const handlePointerDown = (event: PointerEvent) => {
updatePointerPosition(event, { resetVelocity: true });
};
const handlePointerMove = (event: PointerEvent) => {
updatePointerPosition(event, {
resetVelocity: !interaction.pointerInside,
});
};
const handlePointerLeave = () => {
interaction.pointerInside = false;
interaction.pointerVelocityX = 0;
interaction.pointerVelocityY = 0;
};
const handlePointerUp = (event: PointerEvent) => {
updatePointerPosition(event, { resetVelocity: true });
};
const handleWindowBlur = () => {
interaction.pointerInside = false;
interaction.pointerVelocityX = 0;
interaction.pointerVelocityY = 0;
};
canvas.addEventListener('pointerdown', handlePointerDown);
canvas.addEventListener('pointerleave', handlePointerLeave);
canvas.addEventListener('pointermove', handlePointerMove);
canvas.addEventListener('pointerup', handlePointerUp);
window.addEventListener('blur', handleWindowBlur);
const renderFrame = (
_timestamp: DOMHighResTimeStamp,
{ elapsedSeconds }: VisualRenderLoopFrame,
) => {
halftoneMaterial.uniforms.time.value = elapsedSeconds;
halftoneMaterial.uniforms.dashColor.value.set(tuning.halftone.dashColor);
halftoneMaterial.uniforms.s_3.value = tuning.halftone.power;
halftoneMaterial.uniforms.s_4.value = tuning.halftone.width;
halftoneMaterial.uniforms.tile.value = tuning.halftone.scale;
halftoneMaterial.uniforms.waveSpeed.value = tuning.animation.waveSpeed;
imageMaterial.uniforms.contrast.value = tuning.halftone.imageContrast;
interaction.smoothedMouseX +=
(interaction.mouseX - interaction.smoothedMouseX) * IMAGE_POINTER_FOLLOW;
interaction.smoothedMouseY +=
(interaction.mouseY - interaction.smoothedMouseY) * IMAGE_POINTER_FOLLOW;
interaction.pointerVelocityX *= IMAGE_POINTER_VELOCITY_DAMPING;
interaction.pointerVelocityY *= IMAGE_POINTER_VELOCITY_DAMPING;
halftoneMaterial.uniforms.interactionUv.value.set(
interaction.smoothedMouseX,
1 - interaction.smoothedMouseY,
);
halftoneMaterial.uniforms.interactionVelocity.value.set(
interaction.pointerVelocityX * getVirtualWidth(),
-interaction.pointerVelocityY * getVirtualHeight(),
);
halftoneMaterial.uniforms.dragOffset.value.set(0, 0);
halftoneMaterial.uniforms.hoverLightStrength.value =
interaction.pointerInside && tuning.animation.hoverLightEnabled
? tuning.animation.hoverLightIntensity
: 0;
halftoneMaterial.uniforms.hoverLightRadius.value =
tuning.animation.hoverLightRadius;
halftoneMaterial.uniforms.hoverFlowStrength.value = 0;
halftoneMaterial.uniforms.hoverFlowRadius.value = 0.18;
halftoneMaterial.uniforms.dragFlowStrength.value = 0;
imageMaterial.uniforms.zoom.value = getImagePreviewZoom(PREVIEW_DISTANCE);
halftoneMaterial.uniforms.footprintScale.value = getHalftoneScale();
renderer.setRenderTarget(sceneTarget);
renderer.render(imageScene, orthographicCamera);
blurHorizontalMaterial.uniforms.tInput.value = sceneTarget.texture;
renderer.setRenderTarget(blurTargetA);
renderer.render(blurHorizontalScene, orthographicCamera);
blurVerticalMaterial.uniforms.tInput.value = blurTargetA.texture;
renderer.setRenderTarget(blurTargetB);
renderer.render(blurVerticalScene, orthographicCamera);
blurHorizontalMaterial.uniforms.tInput.value = blurTargetB.texture;
renderer.setRenderTarget(blurTargetA);
renderer.render(blurHorizontalScene, orthographicCamera);
blurVerticalMaterial.uniforms.tInput.value = blurTargetA.texture;
renderer.setRenderTarget(blurTargetB);
renderer.render(blurVerticalScene, orthographicCamera);
renderer.setRenderTarget(null);
renderer.clear();
renderer.render(postScene, orthographicCamera);
};
renderLoop = createVisualRenderLoop({
renderFrame,
shouldRender: () => !signal.aborted,
target: container,
targetVisibilityOptions: { rootMargin: '100px' },
});
renderLoop.start();
return () => {
renderLoop?.dispose();
stopObservingSize();
canvas.removeEventListener('pointerdown', handlePointerDown);
canvas.removeEventListener('pointerleave', handlePointerLeave);
canvas.removeEventListener('pointermove', handlePointerMove);
canvas.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('blur', handleWindowBlur);
blurHorizontalMaterial.dispose();
blurVerticalMaterial.dispose();
halftoneMaterial.dispose();
imageMaterial.dispose();
imageTexture.dispose();
fullScreenGeometry.dispose();
sceneTarget.dispose();
blurTargetA.dispose();
blurTargetB.dispose();
renderer.dispose();
if (canvas.parentNode === container) {
container.removeChild(canvas);
}
};
}
@@ -0,0 +1,37 @@
'use client';
import { useEffect, type RefObject } from 'react';
import { mountHalftoneCanvas } from './monolith-halftone-mount';
export function useMonolithHalftone({
imageUrl,
mountRef,
}: {
imageUrl: string;
mountRef: RefObject<HTMLDivElement | null>;
}) {
useEffect(() => {
const container = mountRef.current;
if (!container) {
return;
}
const abortController = new AbortController();
const unmountPromise = mountHalftoneCanvas({
container,
imageUrl,
signal: abortController.signal,
}).catch((error) => {
if (error.name !== 'AbortError') {
console.error(error);
}
return undefined;
});
return () => {
abortController.abort();
void unmountPromise.then((dispose) => dispose?.());
};
}, [imageUrl, mountRef]);
}
@@ -0,0 +1,2 @@
export { Problem } from './Problem';
export type { ProblemPointType } from './problem-point-type';
@@ -0,0 +1,7 @@
import type { MessageDescriptor } from '@lingui/core';
import type { ReactNode } from 'react';
export type ProblemPointType = {
heading: ReactNode;
body: MessageDescriptor;
};