Skip to content

Commit

Permalink
further UI work on anonymisation interface
Browse files Browse the repository at this point in the history
  • Loading branch information
jthrilly committed Dec 6, 2024
1 parent 26d319c commit 9b79713
Show file tree
Hide file tree
Showing 4 changed files with 181 additions and 76 deletions.
202 changes: 131 additions & 71 deletions lib/interviewer/components/EncryptedBackground.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';

type Stream = {
id: number;
Expand All @@ -15,6 +15,7 @@ type Stream = {
scrambleCount: number;
maxScrambles: number;
}[];
lastScrambleTime?: number;
};

const names = [
Expand Down Expand Up @@ -110,122 +111,181 @@ const getRandomChar = (): string => {
return encryptionChars[Math.floor(Math.random() * encryptionChars.length)]!;
};

const createStream = (yPosition = -20) => {
const createStream = (yPosition = -20, thresholdPosition: number) => {
const name = names[Math.floor(Math.random() * names.length)]!;
const shouldBeEncrypted = yPosition > thresholdPosition;

return {
id: Math.random(),
word: name,
x: Math.random() * 100,
y: yPosition,
speed: 0.1 + Math.random() * 0.2,
encrypted: false,
speed: 0.05 + Math.random() * 0.1,
encrypted: shouldBeEncrypted,
lastScrambleTime: 0,
letters: Array.from(name).map((letter) => ({
original: letter,
current: letter,
target: '',
current: shouldBeEncrypted ? getRandomChar() : letter,
target: shouldBeEncrypted ? getRandomChar() : '',
isScrambling: false,
scrambleCount: 0,
maxScrambles: 5 + Math.floor(Math.random() * 5),
})),
};
};

const EncryptionBackground = () => {
type EncryptionBackgroundProps = {
thresholdPosition?: number;
};

const EncryptionBackground = ({
thresholdPosition = 25,
}: EncryptionBackgroundProps) => {
const [streams, setStreams] = useState<Stream[]>([]);
const animationFrameRef = useRef<number>();
const lastUpdateTimeRef = useRef<number>(0);

useEffect(() => {
const initialStreams = Array.from({ length: 40 }, (_, index) =>
createStream(index * 6),
const initialStreams = Array.from({ length: 20 }, (_, index) =>
createStream(index * 6, thresholdPosition),
);
setStreams(initialStreams);

const interval = setInterval(() => {
setStreams((currentStreams) => {
return currentStreams.map((stream) => {
const newY = stream.y + stream.speed;
const updateStreams = (currentTime: number) => {
const timeDelta = currentTime - lastUpdateTimeRef.current;

if (newY > 120) {
return createStream();
setStreams((currentStreams) =>
currentStreams.map((stream) => {
const newY = stream.y + (stream.speed * timeDelta) / 16;

if (newY > 100) {
return createStream(-20, thresholdPosition);
}

const shouldStartEncrypt = Math.random() < 0.01;
const shouldStartDecrypt = Math.random() < 0.01;

const newLetters = stream.letters.map((letterState) => {
if (
shouldStartEncrypt &&
!stream.encrypted &&
!letterState.isScrambling
) {
return {
...letterState,
isScrambling: true,
scrambleCount: 0,
target: getRandomChar(),
};
}
if (
shouldStartDecrypt &&
stream.encrypted &&
!letterState.isScrambling
) {
return {
...letterState,
isScrambling: true,
scrambleCount: 0,
target: letterState.original,
};
}
if (letterState.isScrambling) {
const newScrambleCount = letterState.scrambleCount + 1;
if (newScrambleCount >= letterState.maxScrambles) {
// Check if crossing threshold
const shouldStartEncrypt =
!stream.encrypted &&
stream.y <= thresholdPosition &&
newY > thresholdPosition;

// Force encryption state based on position
const shouldBeEncrypted = newY > thresholdPosition;

const shouldUpdateScramble =
currentTime - (stream.lastScrambleTime || 0) > 50;

let newLetters = stream.letters;
if (shouldUpdateScramble || shouldStartEncrypt) {
newLetters = stream.letters.map((letterState) => {
// Start encryption
if (shouldStartEncrypt && !letterState.isScrambling) {
return {
...letterState,
isScrambling: true,
scrambleCount: 0,
target: getRandomChar(),
};
}
// Continue scrambling
if (letterState.isScrambling && shouldUpdateScramble) {
const newScrambleCount = letterState.scrambleCount + 1;
if (newScrambleCount >= letterState.maxScrambles) {
return {
...letterState,
current: letterState.target,
isScrambling: false,
};
}
return {
...letterState,
current: getRandomChar(),
scrambleCount: newScrambleCount,
};
}
// Force encryption state if below threshold
if (
shouldBeEncrypted &&
!letterState.isScrambling &&
letterState.current === letterState.original
) {
return {
...letterState,
current: letterState.target,
isScrambling: false,
current: getRandomChar(),
target: getRandomChar(),
};
}
return {
...letterState,
current: getRandomChar(),
scrambleCount: newScrambleCount,
};
}
return letterState;
});

const isNowEncrypted = newLetters.every(
(l) => !l.isScrambling && l.current !== l.original,
);
return letterState;
});
}

const isNowEncrypted =
shouldBeEncrypted ||
newLetters.every(
(l) => !l.isScrambling && l.current !== l.original,
);

return {
...stream,
y: newY,
encrypted: isNowEncrypted,
letters: newLetters,
lastScrambleTime: shouldUpdateScramble
? currentTime
: stream.lastScrambleTime,
};
});
});
}, 50);
}),
);

lastUpdateTimeRef.current = currentTime;
animationFrameRef.current = requestAnimationFrame(updateStreams);
};

return () => clearInterval(interval);
}, []);
lastUpdateTimeRef.current = performance.now();
animationFrameRef.current = requestAnimationFrame(updateStreams);

return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [thresholdPosition]);

return (
<div className="pointer-events-none h-full w-full overflow-hidden text-white/30 select-none">
<div
className="preserve-3d pointer-events-none absolute relative inset-0 h-full w-full overflow-hidden text-white/30 select-none"
style={{ perspective: '1000px' }}
>
<div
className="absolute w-full text-center text-[4rem] text-white/100 will-change-transform"
style={{
top: `${thresholdPosition}%`,
background:
'linear-gradient(rgba(255,255, 255, 0) 40%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0) 60%)',
}}
>
🔒
</div>

{streams.map((stream) => (
<div
key={stream.id}
className="absolute font-mono whitespace-nowrap transition-all duration-300"
className="absolute font-mono whitespace-nowrap transition-colors duration-300 will-change-transform"
style={{
left: `${stream.x}%`,
top: `${stream.y}%`,
transform: 'translate(-50%, -50%)',
transform: `translate3d(${stream.x}vw, ${stream.y}vh, 0)`,
opacity: Math.max(0, Math.min(1, (100 - stream.y) / 50)),
color: stream.encrypted ? 'rgb(100, 255, 150, 0.3)' : undefined,
}}
>
{stream.letters.map((letterState, index) => (
<span key={index} className="inline-block">
<span
key={index}
className="inline-block transition-colors duration-300"
style={{
color: letterState.isScrambling
? 'rgb(255, 255, 180, 0.5)'
: undefined,
}}
>
{letterState.current}
</span>
))}
Expand Down
51 changes: 47 additions & 4 deletions lib/interviewer/containers/Interfaces/Anonymisation.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
import { motion } from 'motion/react';
import { type AnonymisationStage } from '~/lib/protocol-validation/schemas/src/8.zod';
import { Markdown } from '~/lib/ui/components/Fields';
import EncryptionBackground from '../../components/EncryptedBackground';
import { type StageProps } from '../Stage';

export default function Anonymisation() {
type AnonymisationProps = StageProps & {
stage: AnonymisationStage;
};

export default function Anonymisation(props: AnonymisationProps) {
console.log(props.stage.items);
return (
<div className="anonymisation flex h-full w-full flex-col">
<EncryptionBackground />
</div>
<>
<motion.div className="anonymisation flex h-full w-full flex-col items-center justify-center">
<motion.div
className="z-10 max-w-[80ch] rounded-(--nc-border-radius) bg-(--form-intro-panel-background) px-[2.4rem] py-[2.4rem]"
initial={{
scale: 0.8,
opacity: 0,
y: 50,
}}
animate={{
scale: 1,
opacity: 1,
y: 0,
}}
// Reduce the spring effect
transition={{
type: 'spring',
damping: 15,
delay: 0.2,
}}
>
<h1 className="text-center">Protect Your Data</h1>
{props.stage.items.map((item) => {
console.log(item);

return <Markdown key={item.id} label={item.content} />;
})}
</motion.div>
</motion.div>
<motion.div
className="absolute inset-0"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<EncryptionBackground />
</motion.div>
</>
);
}
2 changes: 2 additions & 0 deletions lib/protocol-validation/schemas/src/8.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,8 @@ const anonymisationStage = baseStageSchema.extend({
),
});

export type AnonymisationStage = z.infer<typeof anonymisationStage>;

const oneToManyDyadCensusStage = baseStageSchema.extend({
type: z.literal('OneToManyDyadCensus'),
subject: subjectSchema,
Expand Down
2 changes: 1 addition & 1 deletion lib/test-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const protocol: Protocol = {
size: 'MEDIUM',
id: '08964cf2-4c7b-4ecd-a6ef-123456',
content:
'This interview allows you to encrypt the names of the people you mention so that they cannot be seen by anyone but you - even the researchers running this study. \n',
'This interview allows you to encrypt the names of the people you mention so that they cannot be seen by anyone but you - even the researchers running this study. \n\nTo use this feature, click on the padlock icon below, and enter a passcode when prompted.',
type: 'text',
},
],
Expand Down

0 comments on commit 9b79713

Please sign in to comment.