diff --git a/lib/interviewer/components/EncryptedBackground.tsx b/lib/interviewer/components/EncryptedBackground.tsx index c64d4bc7..ea41b8b0 100644 --- a/lib/interviewer/components/EncryptedBackground.tsx +++ b/lib/interviewer/components/EncryptedBackground.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; type Stream = { id: number; @@ -15,6 +15,7 @@ type Stream = { scrambleCount: number; maxScrambles: number; }[]; + lastScrambleTime?: number; }; const names = [ @@ -110,19 +111,22 @@ 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), @@ -130,102 +134,158 @@ const createStream = (yPosition = -20) => { }; }; -const EncryptionBackground = () => { +type EncryptionBackgroundProps = { + thresholdPosition?: number; +}; + +const EncryptionBackground = ({ + thresholdPosition = 25, +}: EncryptionBackgroundProps) => { const [streams, setStreams] = useState([]); + const animationFrameRef = useRef(); + const lastUpdateTimeRef = useRef(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 ( -
+
+
+ 🔒 +
+ {streams.map((stream) => (
{stream.letters.map((letterState, index) => ( - + {letterState.current} ))} diff --git a/lib/interviewer/containers/Interfaces/Anonymisation.tsx b/lib/interviewer/containers/Interfaces/Anonymisation.tsx index 6c2794d8..a879fbdc 100644 --- a/lib/interviewer/containers/Interfaces/Anonymisation.tsx +++ b/lib/interviewer/containers/Interfaces/Anonymisation.tsx @@ -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 ( -
- -
+ <> + + +

Protect Your Data

+ {props.stage.items.map((item) => { + console.log(item); + + return ; + })} +
+
+ + + + ); } diff --git a/lib/protocol-validation/schemas/src/8.zod.ts b/lib/protocol-validation/schemas/src/8.zod.ts index efda315b..0f1a1819 100644 --- a/lib/protocol-validation/schemas/src/8.zod.ts +++ b/lib/protocol-validation/schemas/src/8.zod.ts @@ -448,6 +448,8 @@ const anonymisationStage = baseStageSchema.extend({ ), }); +export type AnonymisationStage = z.infer; + const oneToManyDyadCensusStage = baseStageSchema.extend({ type: z.literal('OneToManyDyadCensus'), subject: subjectSchema, diff --git a/lib/test-protocol.ts b/lib/test-protocol.ts index 8366c8d0..763e2f54 100644 --- a/lib/test-protocol.ts +++ b/lib/test-protocol.ts @@ -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', }, ],